😸

【Python】【pandas】json_normalize()がよくわからない年頃

2025/01/08に公開

https://pandas.pydata.org/docs/reference/api/pandas.json_normalize.html

世界がおかしいのか、私がおかしいのか。
この関数の挙動がおかしいのか、私の理解がおかしいのか。

1.インターフェース概要

pandas.json_normalize(data, record_path=None, meta=None, meta_prefix=None, record_prefix=None, errors='raise', sep='.', max_level=None)

凖構造化JSONデータをフラットテーブルに正規化する関数。JSONデータとあるが、配列や辞書が入れ子構造となっている木構造データをテーブル形式に変換してくれる。

1-1. パラメータ

パラメーター デフォルト 説明(Google翻訳にぶち込んだだけ)
data dict or list of dict - シリアル化されていないJSONオブジェクト。
record_path str or list of str None 各オブジェクト内のレコードリストへのパス。渡されない場合、データはレコード配列であると見なされます。
meta list of paths(str or list of str) None 結果テーブルの各レコードのメタデータとして使用するフィールド。
meta_prefix str None Trueの場合、レコードの前にドット(?)パスが付きます。
たとえば、metaが["foo", "bar"]の場合は、foo.bar.fieldになります。
record_prefix str None True の場合、レコードの先頭にドット (?) パスが付きます。
たとえば、レコードへのパスが [‘foo’, ‘bar’] の場合は foo.bar.field になります。
errors {"raise", "ignore"} "raise" エラー処理を設定します。
ignore’ : メタにリストされているキーが常に存在するとは限らない場合、KeyError を無視します。
‘raise’ : メタにリストされているキーが常に存在するとは限らない場合、KeyError を発生させます。
sep str "." ネストされたレコードは、sep で区切られた名前を生成します。例: sep=’.’, {‘foo’: {‘bar’: 0}} -> foo.bar。
max_level int None 正規化するレベルの最大数 (辞書の深さ)。 None の場合は、すべてのレベルを正規化します。

2. 動かせ!!!!

ごちゃごちゃと細かい事が書いてあるが、実際にドキュメントのサンプルコードを動かしてみれば一発で処理のイメージがつく。

import pandas as pd

data = [
    {
        "id": 1,
        "name": "Cole Volk",
        "fitness": {"height": 130, "weight": 60},
    },
    {
        "name": "Mark Reg",
        "fitness": {"height": 130, "weight": 60}
    },
    {
        "id": 2,
        "name": "Faye Raker",
        "fitness": {"height": 130, "weight": 60},
    },
]
pd.json_normalize(data, max_level=0)
id name fitness
0 1.0 Cole Volk {'height': 130, 'weight': 60}
1 NaN Mark Reg {'height': 130, 'weight': 60}
2 2.0 Faye Raker {'height': 130, 'weight': 60}

続いて、max_level=1を指定するともう一段深い階層で表を作成してくれる。

pd.json_normalize(data, max_level=1)
id name fitness.height fitness.weight
0 1.0 Cole Volk 130 60
1 NaN Mark Reg 130 60
2 2.0 Faye Raker 130 60

続いて、もう少し複雑なJSONデータを扱う。

data = [
    {
        "state": "Florida",
        "shortname": "FL",
        "info": {"governor": "Rick Scott"},
        "counties": [
            {"name": "Dade", "population": 12345},
            {"name": "Broward", "population": 40000},
            {"name": "Palm Beach", "population": 60000},
        ],
    },
    {
        "state": "Ohio",
        "shortname": "OH",
        "info": {"governor": "John Kasich"},
        "counties": [
            {"name": "Summit", "population": 1234},
            {"name": "Cuyahoga", "population": 1337},
        ],
    },
]
pd.json_normalize(
    data,
    record_path="counties",
    meta=["state", "shortname", ["info", "governor"]]
)

record_pathは表にしたいデータを選択する。文字列、または上位からの文字列のリストを指定する。
metaはrecord_path外にあるデータで、表に追加したいものを指定する。ここでは、"state", "shortname", 及び "info" -> "governor"を表に追加する。

name population state shortname info.governor
0 Dade 12345 Florida FL Rick Scott
1 Broward 40000 Florida FL Rick Scott
2 Palm Beach 60000 Florida FL Rick Scott
3 Summit 1234 Ohio OH John Kasich
4 Cuyahoga 1337 Ohio OH John Kasich

3. 問題の挙動

次のようなデータ構造を持つJSONデータを扱う。

{  
    "GET_STATS": {
        "RESULT": { HTTPのResult Codeなど },
        "PARAMETER": { API問い合わせパラメータ },
        "STATISTICAL_DATA": {
            "RESULT_INF": { "TOTAL_NUMBER": 全件数 },
            "TABLE_INF": { "STAT_NAME": 出典 },
            "DATA_INF": {
                "DATA_OBJ": [
                    { "VALUE": { データ1 }},
                    { "VALUE": { データ2 }},
                    { "VALUE": { データ3 }},
                             :
                ]
            }
        }
    }
}

統計ダッシュボードのAPIで取得できる。
https://dashboard.e-stat.go.jp/

import requests

api_url = "https://dashboard.e-stat.go.jp/api/1.0/Json/getData?Lang=JP&IndicatorCode=1402020000000010010&RegionalRank=3"
response = requests.get(api_url)
dict_json = response.json()

JSONデータはDictionary形式なので、次のようにアクセスしていくことで各データへアクセスすることができる。

dict_json["GET_STATS"]["STATISTICAL_DATA"]["DATA_INF"]["DATA_OBJ"][0]["VALUE"]
{'@indicator': '1402020000000010010',
 '@unit': '055',
 '@stat': '00200502',
 '@regionCode': '01000',
 '@time': '1975CY00',
 '@cycle': '3',
 '@regionRank': '3',
 '@isSeasonal': '1',
 '@isProvisional': '0',
 '$': '78502'}

ここで、もしデータ構造が次のようにアクセス可能であれば、コンストラクタから比較的楽にDataFrameを作成できる。

dict_json["GET_STATS"]["STATISTICAL_DATA"]["DATA_INF"]["DATA_OBJ"]["VALUE"][0]

今回はコンストラクタから楽にDataFrameインスタンスを生成できないので、pd.json_normalize()を使ってDataFrameを作成する。

pd.json_normalize(
    dict_json["GET_STATS"]["STATISTICAL_DATA"],
    record_path=["DATA_INF", "DATA_OBJ"],
    max_level=2,
    meta=[
        ["RESULT_INF"],
        ["TABLE_INF"],
    ],
)
VALUE.@indicator VALUE.@unit VALUE.@stat VALUE.@regionCode VALUE.@time VALUE.@cycle VALUE.@regionRank VALUE.@isSeasonal VALUE.@isProvisional VALUE.$ RESULT_INF TABLE_INF
0 1402020000000010010 055 00200502 01000 1975CY00 3 3 1 0 78502 {'TOTAL_NUMBER': '2209'} {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}
1 1402020000000010010 055 00200502 02000 1975CY00 3 3 1 0 11876 {'TOTAL_NUMBER': '2209'} {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}
2 1402020000000010010 055 00200502 03000 1975CY00 3 3 1 0 9472 {'TOTAL_NUMBER': '2209'} {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}

(略)

ここまではいい。
metaで追加した列がDictionaryオブジェクトになっているので、まずはTOTAL_NUMBERの値をそのまま出してみよう。
※注:TOTAL_NUMBERを表に出力する目的はjson_normalize()を使って色々試すことであり、全件数が必要というわけではありません。

pd.json_normalize(
    dict_json["GET_STATS"]["STATISTICAL_DATA"],
    record_path=["DATA_INF", "DATA_OBJ"],
    meta=[
        ["RESULT_INF", "TOTAL_NUMBER"],
        ["TABLE_INF"],
    ],
)
KeyError: "Key 'TOTAL_NUMBER' not found. To replace missing values of 'TOTAL_NUMBER' with np.nan, pass in errors='ignore'"

なぜぇぇぇぇぇぇーーーーー!!!

パラメーターにerrors="ignore"を指定すれば、このエラーは回避されるが、どうやらTOTAL_NUMBERの値が認識されないみたいでNaNとなってしまう。

pd.json_normalize(
    dict_json["GET_STATS"]["STATISTICAL_DATA"],
    record_path=["DATA_INF", "DATA_OBJ"],
    meta=[
        ["RESULT_INF", "TOTAL_NUMBER"],
        ["TABLE_INF"],
    ],
    errors="ignore",
)
VALUE.@indicator VALUE.@unit VALUE.@stat VALUE.@regionCode VALUE.@time VALUE.@cycle VALUE.@regionRank VALUE.@isSeasonal VALUE.@isProvisional VALUE.$ RESULT_INF.TOTAL_NUMBER TABLE_INF
0 1402020000000010010 055 00200502 01000 1975CY00 3 3 1 0 78502 nan {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}
1 1402020000000010010 055 00200502 02000 1975CY00 3 3 1 0 11876 nan {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}
2 1402020000000010010 055 00200502 03000 1975CY00 3 3 1 0 9472 nan {'STAT_NAME': [{'@code': '00200502', '$': '社会・人口統計体系'}]}

json_normalize()のsourceをコピペしてデバッグ実行して確認してみたところ、GET_STATS.STATISTICAL_DATA.RESULT_INF.TOTAL_NUMBERというKeyで、値の問い合わせをGET_STATS.STATISTICAL_DATA.TABLE_INFに対して行っているため、値が見つからずにNaNになっているように見受けられるが・・・

これ以上時間を使って調べても、な感じなので、
「なんか使えない場合もある」ぐらいの認識で私の旅はここで終わろうと思います。

4. 結論

この記事ってなんか意味あるの?

Discussion