naoのブログ ~エンジニアスキルに生成演算子を作用させたい~

日々アウトプットしていくことを目標に、だらだらと書いていきます。

estatからデータを取得してみる

estatのデータの取得と整理

matplotlibで利用するためのデータを取得するために、 外部のサイトからデータをAPIで取得して、そのデータをもとに図を書くということをしてみたいと思いました。
(今回の内容はデータを取得して、必要な形に整理するところまでです。matplotlibでのグラフ作成までは一度に整理できなかったのでまた今度です。)

既に、以下のサイトの記事で、estatのAPIを取得してから データを取り出すために試行錯誤するところについて 丁寧に記載されていますが、 今回は自分用のメモとして記事にしようと思います。

今回は利用するオープンデータとして、estatを使用しました。 ここから国勢調査などの国が提供しているデータを利用できます。

初めてやることなので、estatの一番上にあるデータである国勢調査 / 平成27年国勢調査 / 人口等基本集計(男女・年齢・配偶関係,世帯の構成,住居の状態など)を取得してみます。
ちなみに、APIで利用するURLは、estatのマイページから確認できます。

APIで取得できるデータ構造

データはjson形式で階層構造を以下のようになっています。 この中で、データとして活用する機会があるのは CLASS_OBJとVALUEです。

VALUEには目的のデータが含まれており、 CLASS_OBJにはデータとして使用する値の定義情報が 含まれています。

GET_STATS_DATA
  |- PARAMETER
  |
  |- RESULT
  |
  |- STATISTICAL_DATA
       |
       |- CLASS_INF
       |    |
       |    |- CLASS_OBJ ☆ ここにあるデータを使う
       |
       |- DATA_INF
            |
            |- VALUE ☆ ここにあるデータを使う

APIからの取得は以下のように、上記サイトで指定されるURLを利用して、 urllib.request.urlopen(url)を使って取得します。
今回の場合は、urlにはestatが公開している「APIリクエストURL」を指定します。

また、取得したデータをファイルに保存したときに、 フォーマットが整った形でファイルに保存したい場合には、 json.dump()を使用します。

# coding: utf-8
import pandas as pd
import urllib.request
from io import StringIO
from IPython.core.display import display
import json

appid = "自分のIDを指定"
json_url = "https://api.e-stat.go.jp/rest/2.1/app/json/getStatsData?"
opt_url = "&lang=J&statsDataId=0003148500&metaGetFlg=Y&cntGetFlg=N&sectionHeaderFlg=1"
url = json_url + "appId=" + appid + opt_url
print(url)

def get_json_data(url, file_name):
  res_str = urllib.request.urlopen(url).read().decode('utf-8')
  res = json.loads(res_str)
  with open(file_name, mode="w", encoding='utf-8') as f:
    json.dump(res, f, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': '))
    print("save data to {0}".format(file_name))
  return pd.read_json(file_name, encoding='utf-8')
json_data = get_json_data(url, "tmp.json")

取得されるjsonのデータは以下のものです。
※ 一部省略しているデータ部分は「// 省略」と記載しています。
このjson形式のデータを利用するには、ここから「GET_STATS_DATA → STATISTICAL_DATA → DATA_INF → VALUE」のようにkeyを指定していって、値を取り出す必要があります。

{
    "GET_STATS_DATA": {
        "PARAMETER": {
          // 今回は重要でないため省略
        },
        "RESULT": {
            "DATE": "2019-05-11T13:23:06.240+09:00",
            "ERROR_MSG": "正常に終了しました。",
            "STATUS": 0
        },
        "STATISTICAL_DATA": {
            "CLASS_INF": {
                "CLASS_OBJ": "[{'@id': 'tab', '@name': '表章項目..."  // ...: データが多いため一部省略
            },
            "DATA_INF": {
                "NOTE": [
                  // 今回は重要でないため省略
                ],
                "VALUE": "[{'@tab': '020', '@cat01': '00..." // ...: データが多いため一部省略
            },
            "RESULT_INF": {
                "FROM_NUMBER": 1,
                "TOTAL_NUMBER": 63530,
                "TO_NUMBER": 63530
            },
            "TABLE_INF": {
              // 今回は重要でないため省略
            }
        }
    }
}

STATISTICAL_DATA → DATA_INF → VALUE について

VALUEには、目的のデータが入れられています。 まずは、今回のAPIで得られたこれらのデータを確認していきます。

DATA_INFのVALUEには、データがlist ([...])として格納されています。 また、要素にはdict型{...}の形式で1つ1つデータが格納されています。

以下のものは2つの要素だけを取り出した結果です。 1つめの{...}の$には127,094,745の値が入っています。
(後でみるように日本の総人口の値を示していることが分かりますが)

[{'$': '127094745',
  '@area': '00000',
  '@cat01': '00710',
  '@tab': '020',
  '@time': '2015000000',
  '@unit': '人'},
 {'$': '116137232',
  '@area': '00001',
  '@cat01': '00710',
  '@tab': '020',
  '@time': '2015000000',
  '@unit': '人'}]

この値が具体的に何を示しているかについては、 @area、@cat01、@tab、@timeを見て、判断する必要があります。

これら(@area、@cat01、@tab、@time)の定義については、次のCLASS_OBJに含まれています。

ちなみに、json形式の上記のデータをDataFrameにいれて、 head()で先頭5つを取り出すと以下のようになります。

$ @area @cat01 @tab @time @unit
0 127094745 00000 00710 020 2015000000
1 116137232 00001 00710 020 2015000000
2 10957513 00002 00710 020 2015000000
3 5381733 01000 00710 020 2015000000
4 4395172 01001 00710 020 2015000000

STATISTICAL_DATA → DATA_INF → VALUEのデータをDataFrameに入れるには、 json_dataからkeyを順に指定していき、取り出した値をDataFrameに渡します。

data_VALUE = json_data['GET_STATS_DATA']['STATISTICAL_DATA']['DATA_INF']['VALUE']
df_VALUE = pd.DataFrame(data_VALUE)
display(df_VALUE.head())

STATISTICAL_DATA → CLASS_INF → CLASS_OBJ について

今度はCLASS_OBJについて詳しく見ていきます。
CLASS_OBJには、データに使用する値の情報が入っています。
具体的には国勢調査 / 平成27年国勢調査 / 人口等基本集計(男女・年齢・配偶関係,世帯の構成,住居の状態など)に記載されている「表章項目、全域・人口集中地区(2015)、地域(2015)、時間軸(年次)」のデータが入っています。

@id @name CLASS
0 tab 表章項目 [{'@code': '020', '@level': '', '@name': '人口',...
1 cat01 全域・人口集中地区(2015) [{'@code': '00710', '@level': '1', '@name': '全...
2 area 地域(2015) [{'@code': '00000', '@level': '1', '@name': '全...
3 time 時間軸(年次) {'@code': '2015000000', '@level': '1', '@name'...

さらに、この中の「地域(2015)」のデータをのぞいてみると、 以下のようにデータを持っているようです。 codeはprimary keyのようで、levelで都道府県、市町村などの単位を分けているようです。
何と都道府県、市町村などが一緒に含まれるテーブル構造になっています...

@code @level @name @parentCode
0 00000 1 全国 NaN
1 00001 1 全国市部 NaN
2 00002 1 全国郡部 NaN
3 01000 2 北海道 00000
4 01001 3 北海道市部 01000
5 01002 3 北海道郡部 01000
6 01100 4 札幌市 01000
7 01101 5 札幌市 中央区 01100
8 01102 5 札幌市 北区 01100
9 01103 5 札幌市 東区 01100

上記のデータを確認するには、以下のようにします。 STATISTICAL_DATA → CLASS_INF → CLASS_OBJのデータをDataFrameに入れるには、 json_dataからkeyを順に指定していき、取り出した値をDataFrameに渡します。
CLASS_OBJ内にある「@id==areaのCLASS」を取得するには、行と列の値を指定します。
今回は、列の取得をdf['列名']でしてから、行の取得をloc('行数')でしています。

data_CLASS_OBJ = json_data['GET_STATS_DATA']['STATISTICAL_DATA']['CLASS_INF']['CLASS_OBJ']
df_CLASS_OBJ = pd.DataFrame(data_CLASS_OBJ)
display(df_CLASS_OBJ.head())

# @id==areaのCLASSの情報を確認
df_CLASS_OBJ_CLASS_area = pd.DataFrame(df_CLASS_OBJ['CLASS'].loc[2])
display(df_CLASS_OBJ_CLASS_area.head(n=10))

[補足] CLASS_OBJについて

ここでは、もう少しCLASS_OBJについてみておきます。
上でみたように、この中には@idの列でいうとtab, cat01, area, timeの4つが入っています。例えば、tabとareaは以下のようになっています。
tabではそれぞれの統計情報が一つにまとまっており、 areaではlevelに基づいて、都道府県のスケールや市町村のスケールで の結果が混ざり合っています。
なので、実際にデータを取り扱う際には、不要なデータには フィルターをかけて除外する必要があります。

[tab]について

@code @level @name @unit
0 020 人口
1 106 組替人口(平成22年)
2 107 平成22年~27年の人口増減数
3 108 平成22年~27年の人口増減率
4 103 面積 平方km
5 104 人口密度 NaN
6 109 世帯数 世帯
7 110 組替世帯数(平成22年) 世帯
8 111 平成22年~27年の世帯数増減数 世帯
9 112 平成22年~27年の世帯数増減率

[area]について

@code @level @name @parentCode
0 00000 1 全国 NaN
1 00001 1 全国市部 NaN
2 00002 1 全国郡部 NaN
3 01000 2 北海道 00000
4 01001 3 北海道市部 01000
5 01002 3 北海道郡部 01000
6 01100 4 札幌市 01000
7 01101 5 札幌市 中央区 01100

データのマージ

さて、DATA_INF内のVALUEに話は戻りますが、このままだと @area、@cat01、@tab、@timeの値がそれぞれID(数字)のままなので 具体的に何の値を示しているかわかりません。

そこで、ここではCLASS_OBJのデータとマージして具体的な値に置き換えていきます。 やることは、DataFrameのmerge()を用いて、それぞれをマージしていくことだけです。 (データの整理についてはあまり良い方法が見つからなかったので、drop()とrename()で強引に整えています。。。)。
これを実行すると、以下に意味のあるデータに変換できたものが取得できます。

なお、取得したデータは、df.to_csv()を使うことで、 csv形式のファイルとして保存できます

$ @area_level @area @cat01 @tab @time
0 127094745 1 全国 全域 人口 2015年
1 116137232 1 全国市部 全域 人口 2015年
2 10957513 1 全国郡部 全域 人口 2015年
3 5381733 2 北海道 全域 人口 2015年
4 4395172 3 北海道市部 全域 人口 2015年

以下の手順で上記のようにデータを整理できます。

# merge data
# もっと効率のよくて賢い方法があると思います。。。
def merge_data(df_VALUE, df_CLASS_OBJ):
  df_CLASS_OBJ_CLASS_tab = pd.DataFrame(df_CLASS_OBJ['CLASS'].loc[0])
  df_CLASS_OBJ_CLASS_cat01 = pd.DataFrame(df_CLASS_OBJ['CLASS'].loc[1])
  df_CLASS_OBJ_CLASS_area = pd.DataFrame(df_CLASS_OBJ['CLASS'].loc[2])
  df_CLASS_OBJ_CLASS_time = pd.DataFrame([df_CLASS_OBJ['CLASS'].loc[3]])
  # time: DataFrame にはdict型のデータを持つリストを渡す必要がある
  print("the original data is below")
  display(df_VALUE.head())

  df = df_VALUE.drop(columns=['@unit'])
  # merge '@area'
  df = pd.merge(df, df_CLASS_OBJ_CLASS_area, how='left', 
              left_on='@area', right_on='@code')
  df = df.drop(columns=['@code', '@parentCode']) # @level is used for classification
  df = df.drop(columns=['@area'])
  df = df.rename(columns={'@name':'@area', '@level':'@area_level'})
  # merge '@cat01'
  df = pd.merge(df, df_CLASS_OBJ_CLASS_cat01, how='left', 
              left_on='@cat01', right_on='@code')
  df = df.drop(columns=['@code', '@level'])
  df = df.drop(columns=['@cat01'])
  df = df.rename(columns={'@name':'@cat01'})
  # merge '@tab'
  df = pd.merge(df, df_CLASS_OBJ_CLASS_tab, how='left', 
              left_on='@tab', right_on='@code')
  df = df.drop(columns=['@code', '@level', '@unit'])
  df = df.drop(columns=['@tab'])
  df = df.rename(columns={'@name':'@tab'})
  # merge '@time'
  df = pd.merge(df, df_CLASS_OBJ_CLASS_time, how='left', 
              left_on='@time', right_on='@code')
  df = df.drop(columns=['@code', '@level'])
  df = df.drop(columns=['@time'])
  df = df.rename(columns={'@name':'@time'})

  print("the merged data is below")
  display(df.head())
  return df
df = merge_data(df_VALUE, df_CLASS_OBJ)

# save data
df.to_csv("organized_data.csv")

整理したデータについて

整理して得られたデータについて、1つ確認してみます。 上記でマージしたデータを使って、都道府県ごとの人口データを取得します。 @area_levelを2(都道府県)、@cat01を全域、@tabを人口となっている行だけを取得します。

必要なデータを抽出すると、以下の表の形の結果が得られます。

$ @area_level @area @cat01 @tab @time
0 5381733 2 北海道 全域 人口 2015年
1 1308265 2 青森県 全域 人口 2015年
2 1279594 2 岩手県 全域 人口 2015年
3 2333899 2 宮城県 全域 人口 2015年
4 1023119 2 秋田県 全域 人口 2015年
5 1123891 2 山形県 全域 人口 2015年
6 1914039 2 福島県 全域 人口 2015年
7 2916976 2 茨城県 全域 人口 2015年

統計情報を確認してみると、次のことが読み取れるので、 きちんと目的のデータになっていることがわかります。

  • count数より、47行分のデータが存在すること

  • @areaのuniqueの値より、47行すべてが異なる@areaであること

  • @tabのuniqueの値より、すべての行が人口に関するデータであること

$ @area_level @area @cat01 @tab @time
count 47 47 47 47 47 47
unique 47 1 47 1 1 1
top 2304264 2 鹿児島県 全域 人口 2015年
freq 1 47 1 47 47 47
# (1) 都道府県ごとの人口データを取得
_tmp_df = df[ (df['@area_level'] == '2') \
            & (df['@tab'] == '人口')     \
            & (df['@cat01'] == '全域')].reset_index(drop=True)
display(_tmp_df)
display(_tmp_df.describe())

次は、取得したデータをもとに、matplolibを用いてグラフを書いていこうと思います。