pandasのjson_normalizeで辞書のリストをDataFrameに変換

Modified: | Tags: Python, pandas, JSON

pandas.json_normalize()を使うと共通のキーをもつ辞書のリストをpandas.DataFrameに変換できる。

Web APIなどで取得できるJSONによく使われる形式なので、それをpandas.DataFrameに変換できるのは非常に便利。

ここでは以下の内容について説明する。

  • pandas.DataFrame()による変換
  • pandas.json_normalize()の基本的な使い方
  • より複雑な場合: 引数record_path, meta
  • JSON文字列・ファイルの一部を読み込み

辞書やリストからなるオブジェクトではなくJSON形式の文字列やファイルを直接pandas.DataFrameとして読み込むにはpandas.read_json()を使う。

なお、pandas.json_normalize()となったのはpandas1.0.0からで、それ以前のバージョンではpandas.io.json.json_normalize()として提供されていた。1.2.2時点ではpandas.io.json.json_normalize()もまだ使えるが、Deprecated(非推奨)となっており、警告(FutureWarning)が出る。

pandas.DataFrame()による変換

以下のような辞書のリストを例とする。

import pandas as pd

l_simple = [{'name': 'Alice', 'age': 25},
            {'name': 'Bob'}]

このようなシンプルな辞書のリストはpandas.DataFrame()で変換可能。

辞書のキーkeyが列ラベル(列名)になり、キーが存在しない場合の要素は欠損値NaNとなる。

print(pd.DataFrame(l_simple))
#     name   age
# 0  Alice  25.0
# 1    Bob   NaN

pandas.json_normalize()でも同じ結果。

print(pd.json_normalize(l_simple))
#     name   age
# 0  Alice  25.0
# 1    Bob   NaN

pandas.json_normalizeの基本的な使い方

辞書の値valueとして辞書を持つ、ネストした(入れ子になった)辞書のリストを例とする。

l_nested = [{'name': 'Alice', 'age': 25, 'id': {'x': 2, 'y': 8}},
            {'name': 'Bob', 'id': {'x': 10, 'y': 4}}]

pandas.DataFrame()を使うと、値の辞書がそのまま要素として変換されてしまう。

print(pd.DataFrame(l_nested))
#     name   age                 id
# 0  Alice  25.0   {'x': 2, 'y': 8}
# 1    Bob   NaN  {'x': 10, 'y': 4}

pandas.json_normalize()を使うと、ネストした辞書もキーごとに個別の列として変換される。

print(pd.json_normalize(l_nested))
#     name   age  id.x  id.y
# 0  Alice  25.0     2     8
# 1    Bob   NaN    10     4

ネストしている部分はデフォルトでは<親のキー>.<子のキー>が列名となる。このセパレータ.は引数sepで変更できる。

print(pd.json_normalize(l_nested, sep='_'))
#     name   age  id_x  id_y
# 0  Alice  25.0     2     8
# 1    Bob   NaN    10     4

より複雑な場合: 引数record_path, meta

以下のように辞書の値が辞書のリストになっている場合。

l_complex = [{'label': 'X',
              'info' : {'n': 'nx', 'm': 'mx'},
              'data': [{'a': 1, 'b': 2},
                       {'a': 3, 'b': 4}]},
             {'label': 'Y',
              'info' : {'n': 'ny', 'm': 'my'},
              'data': [{'a': 10, 'b': 20},
                       {'a': 30, 'b': 40}]}]

デフォルトだと、辞書のリストがそのまま要素となる。

print(pd.json_normalize(l_complex))
#   label                                      data info.n info.m
# 0     X      [{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]     nx     mx
# 1     Y  [{'a': 10, 'b': 20}, {'a': 30, 'b': 40}]     ny     my

引数record_pathでキーを指定すると、そのキーに対応する値のみを対象として変換する。引数record_prefixで列名にプレフィックスを追加可能。

print(pd.json_normalize(l_complex, record_path='data'))
#     a   b
# 0   1   2
# 1   3   4
# 2  10  20
# 3  30  40

print(pd.json_normalize(l_complex, record_path='data', record_prefix='data_'))
#    data_a  data_b
# 0       1       2
# 1       3       4
# 2      10      20
# 3      30      40

他のキーの値も変換したい場合は引数metaで指定する。引数meta_prefixで列名にプレフィックスを追加可能。

print(pd.json_normalize(l_complex, record_path='data',
                        meta='label'))
#     a   b label
# 0   1   2     X
# 1   3   4     X
# 2  10  20     Y
# 3  30  40     Y

print(pd.json_normalize(l_complex, record_path='data',
                        meta='label', meta_prefix='meta_'))
#     a   b meta_label
# 0   1   2          X
# 1   3   4          X
# 2  10  20          Y
# 3  30  40          Y

metaで指定するキーのペアとなる値が辞書の場合は、リスト[[<親のキー>, <子のキー>], ...]で子のキーを指定できる。デフォルトでは<親のキー>.<子のキー>が列名となるが、ここでも引数sepでセパレータを変更できる。

print(pd.json_normalize(l_complex, record_path='data',
                        meta='info'))
#     a   b                    info
# 0   1   2  {'n': 'nx', 'm': 'mx'}
# 1   3   4  {'n': 'nx', 'm': 'mx'}
# 2  10  20  {'n': 'ny', 'm': 'my'}
# 3  30  40  {'n': 'ny', 'm': 'my'}

print(pd.json_normalize(l_complex, record_path='data',
                        meta=[['info', 'n'], ['info', 'm']]))
#     a   b info.n info.m
# 0   1   2     nx     mx
# 1   3   4     nx     mx
# 2  10  20     ny     my
# 3  30  40     ny     my

print(pd.json_normalize(l_complex, record_path='data',
                        meta=[['info', 'n'], ['info', 'm']],
                        sep='_'))
#     a   b info_n info_m
# 0   1   2     nx     mx
# 1   3   4     nx     mx
# 2  10  20     ny     my
# 3  30  40     ny     my

この例のすべての要素をpandas.DataFrameに変換するには、以下のように設定する。

print(pd.json_normalize(l_complex, record_path='data',
                        meta=['label', ['info', 'n'], ['info', 'm']],
                        sep='_'))
#     a   b label info_n info_m
# 0   1   2     X     nx     mx
# 1   3   4     X     nx     mx
# 2  10  20     Y     ny     my
# 3  30  40     Y     ny     my

子のキーを単独で指定する場合でもmeta=[[<親のキー>, <子のキー>]]のようにリストのリストにする必要があるので注意。meta=[<親のキー>, <子のキー>]だとエラー。

print(pd.json_normalize(l_complex, record_path='data',
                        meta=[['info', 'n']]))
#     a   b info.n
# 0   1   2     nx
# 1   3   4     nx
# 2  10  20     ny
# 3  30  40     ny

# print(pd.json_normalize(l_complex, record_path='data',
#                         meta=['info', 'n']))
# KeyError: "Try running with errors='ignore' as key 'n' is not always present"

JSON文字列・ファイルの一部を読み込み

辞書のリストはWeb APIなどで取得できるJSONによくある形式だと最初に書いたが、このままの形で取得できることは少なく、JSON文字列・ファイルの一部(要素)を抽出する必要がある場合が多い。

以下のJSON形式の文字列を例に説明する。

import pandas as pd
import json

s = '{"OTHER": "x", "DATA": [{"name":"Alice","age":25},{"name":"Bob","age":42}]}'

JSON文字列やファイルをpandas.DataFrameに変換する関数としてpandas.read_json()があるが、辞書が入れ子になっていると以下のように辞書がそのまま要素となる、

print(pd.read_json(s))
#   OTHER                          DATA
# 0     x  {'name': 'Alice', 'age': 25}
# 1     x    {'name': 'Bob', 'age': 42}

pandas.json_normalize()を利用するために、まず、標準ライブラリのjsonモジュールのjson.loads()で文字列を辞書やリストからなるオブジェクトに変換する。JSONファイルを読み込む場合はjson.load()を使う。

d = json.loads(s)
print(d)
# {'OTHER': 'x', 'DATA': [{'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 42}]}

print(type(d))
# <class 'dict'>

引数record_pathmetaを設定すると、pandas.json_normalize()pandas.DataFrameに変換できる。

print(pd.json_normalize(d))
#   OTHER                                               DATA
# 0     x  [{'name': 'Alice', 'age': 25}, {'name': 'Bob',...

print(pd.json_normalize(d, record_path='DATA'))
#     name  age
# 0  Alice   25
# 1    Bob   42

print(pd.json_normalize(d, record_path='DATA', meta='OTHER'))
#     name  age OTHER
# 0  Alice   25     x
# 1    Bob   42     x

一部分のみをpandas.DataFrameに変換したいのであれば、オブジェクトから読み込みたい部分のみを抽出してもよい。ネストが深い場合は[キー名][キー名]のように繰り返す。

上述のように、入れ子になっていない辞書のリストはpandas.DataFrame()でも変換可能。

print(d['DATA'])
# [{'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 42}]

print(type(d['DATA']))
# <class 'list'>

print(pd.DataFrame(d['DATA']))
#     name  age
# 0  Alice   25
# 1    Bob   42

print(pd.json_normalize(d['DATA']))
#     name  age
# 0  Alice   25
# 1    Bob   42

関連カテゴリー

関連記事