APIからJSONを取得してCSVファイルで連携するデータ収集のPython実装を試行錯誤してみた

2022.08.29

データアナリティクス事業本部の鈴木です。

データ収集をする際に、データソースのAPIからのレスポンスがJSONで、データレイクへの入力がCSVファイルであるような場合、JSONからCSVへのフォーマット変換が必要になります。 私はPythonを使い慣れているので、LambdaにPythonのコードを置いてデータ収集機能を実現するような場合、Pythonコードをどのように実装すれば良さそうか試したのでまとめてみました。

条件・構成について

以下のような構成で、例えばLambdaに設定するPythonの実装を考えました。

想定した構成

Lambda上にPythonでAPIからデータを取得し、CSVへのフォーマット変換してS3に配置するような機能です。今回はPythonコード部分にだけ注目しています。 また、簡単のためLambdaのローカル上に一度CSVファイルを出力し、boto3のupload_fileなどでアップロードする想定としました。

環境

以下の環境で検証しました。

  • コンテナ:jupyter/datascience-notebook
  • Python:3.10.4
  • Pandas:1.4.2

やってみた

3通りの方法について試してみました。

  1. Pandasを使ってDataFrameに変換して扱う方法
    • json_normalizeを使ってDataFrameに変換する方法
    • read_jsonを使ってDataFrameに変換する方法
  2. リストのリストにして扱う方法
    • 標準のCSVモジュールを使ってCSVファイルにする方法

データは、レスポンスとして辞書のリストが返ってくるイメージで、以下を変数として定義しました。

# https://pandas.pydata.org/docs/reference/api/pandas.json_normalize.html を参考にしました。
data = [
    {"id": 1, "name": {"first": "Coleen", "last": "Volk"}},
    {"name": {"given": "Mark", "family": "Regner"}},
    {"id": 2},
]

ライブラリは以下のように読み込んでおきました。

import csv
import json

import pandas as pd

1. Pandasを使ってDataFrameに変換して扱う方法

json_normalizeを使ってDataFrameに変換する方法

まずjson_normalizeを使って読み込んでみます。json_normalizeはJSONの構造を解析し、DataFrameに変換してくれます。

df = pd.json_normalize(data)
df

json_normalizeでできたDataFrame

メリットは実装側で取得するJSONの構造を理解していなくても、この関数でよしなに展開してくれるため、データソース側で欲しいデータに関係しない多少の構造の変更があったとしても影響を受けにくい点です。

デメリットとしては、id列がfloatにキャストされる場合あがあることです。以下のNullable integer data typeのページにも記載がありますが、NaNが入っている場合、整数型のカラムの値はfloatに変換されてしまいます。

CSVに変換する前になにかしらの方法でケアしてあげれば良いですが、気づかないままto_csvなどで出力してしまうと、元のJSONとの間に差分が出る可能性があります。

何も考えずにto_csvで出力してみます。

df.to_csv("./csv_json_normalize.csv", index=False)

すると結果は以下のようにid列が小数点付きで出ます。

csv_json_normalize.csv

id,name.first,name.last,name.given,name.family
1.0,Coleen,Volk,,
,,,Mark,Regner
2.0,,,,

なお、id列に欠損値がない場合は、整数のままになる挙動をしますが、試したバージョンだとjson_normalizeに型の指定をするオプションがないので、厳密には保証されなさそうです。

以下のような場合は、整数型のままになりました。

# id列にNaNが入っていないデータ
data2 = [
    {"id": 1, "name": {"first": "Coleen", "last": "Volk"}},
    {"id": 2},
]

# DataFrameに変換する
pd.json_normalize(data2)

欠損値がない場合の変換結果

read_jsonを使ってDataFrameに変換する方法

read_jsonを使ってDataFrameに変換することも可能です。

read_jsonを使うメリットは、オプションにdtypeを指定できる点です。

# サンプルデータの一つ目だけ読み込む
df = pd.read_json(json.dumps([data[0]]), orient='records', dtype={"id": "int8"})
df

read_jsonを使ったDataFrame

デメリットは、上記のDataFrameでも分かるよう、ネストされた中身はそのまま出てくる点です。json_normalizeと違ってネストの深い階層まで解析して、よしなに展開してくれる訳ではなく、後から自分でケアする必要があります。

df["name.first"] = df["name"].map(lambda x: x.get("first") if not pd.isna(x) else x)
df["name.last"] = df["name"].map(lambda x: x.get("last") if not pd.isna(x) else x)
df

追加で加工したDataFrame

また、上の表示から分かるように、NaNが含まれていると結局floatに変換されてしまいます。辞書のリストで渡される際には、関心のあるカラムに欠損値が含まれていないか確認し、含まれている場合は避けておいてCSVにする際に後から結合するなど工夫する必要があります。

DataFrameを経由している限りはこの問題は避けられなさそうなので、ID列などが絶対に欠損しないことを保証できている際や、データウェアハウス作成でクレンジングすることを前提に使うことになりそうです。

2. リストのリストにして扱う方法

今回は辞書のリストの形式でレスポンスが返ってくる想定をしているので、DataFrameを経由せずとも、自作の処理で必要な値をリストのリストに展開して、標準のCSVモジュールで書き出すこともできます。

例えば以下のような処理を考えます。

def parse_dict(d):
    """ 辞書から必要な値のリストを作成する
    """
    data_flatten = [d.get("id"), d.get("name", {}).get("first"), d.get("name", {}).get("last")]
    return data_flatten

# 辞書をフラットにする
data_flatten = [parse_dict(d) for d in data]

# データをCSVファイルに書き込む
with open('./csv_standard_module.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(["id", "name_first", "name_last"])
    writer.writerows(data_flatten)

作成されたCSVファイルは以下のようになります。

csv_standard_module.csv

id,name_first,name_last
1,Coleen,Volk
,,
2,,

メリットは、意図しない型変換が起きないことです。デメリットとして、辞書からリストへ必要な値を取り出す処理は自分で記述する必要が出てきます。データ収集機能から叩くAPIはそこまでレスポンスに含まれるデータの仕様は変わらないと思うので、よほど大変な構造のデータが返ってこない限りは、このように愚直に変換の実装を書いてみてもよさそうでした。

最後に

今回はAPIからJSONのデータを取得するデータ収集機能について、CSVファイルに変換する際のPythonの実装例を考えてみました。

Pandasの機能を使えば実装負荷が下がるので楽なものの、DataFrameに変換する際に整数がfloatにキャストされてしまうケースがあるため、注意が必要なことを確認しました。

また、自分で変換を書くことで、手間はかかるものの意図しない変換の可能性を抑えて出力できることも確認しました。

同様の実装を考えておられる方の参考になりましたら幸いです。