【データ処理】APIから取得したJSONデータをDataFrameに変換する基本

準備・環境構築

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

金融APIからデータを取得するところまでは進んでいるものの、返ってきたJSON(JavaScript Object Notation)の構造が複雑で、どうDataFrameに変換すればよいか迷っている段階でしょう。

APIレスポンスは多くの場合JSON形式で返されます。しかし金融APIのJSONはネスト(入れ子)が深く、そのままではpandasのDataFrameに変換できません。

pd.DataFrame()に渡したらエラーになった」「一部のカラムが辞書のまま残った」という経験は、ほぼ全員が通る道です。

原因は、JSONの階層構造(ネスト)をフラット(一次元の表形式)に展開する処理が抜けているためです。ネストの深さやリスト・辞書の混在パターンに応じて、使うべき関数が異なります。

本記事では、Python標準のjsonモジュールからpandasjson_normalizeまで、JSONをDataFrameに変換する5つのパターンを段階的に解説します。実際の金融APIレスポンスに近いサンプルデータを使い、コピペで動くコードを提示します。

手元のJSON変換に悩んだら、パターンを照合するだけで正しい変換方法が見つかる構成にしています。

JSONの基本構造と変換の考え方

JSONの3つの構成要素

JSONは以下の3つの要素で構成されています。

* オブジェクト(Object){}で囲まれたキーと値のペア。Pythonではdictに対応する

* 配列(Array)[]で囲まれた値のリスト。Pythonではlistに対応する

* プリミティブ値:文字列・数値・真偽値・null。PythonではstrintfloatboolNoneに対応する

金融APIのレスポンスは「配列の中にオブジェクトが並ぶ」パターンが最も多く、これが基本形です。問題になるのは、オブジェクトの中にさらにオブジェクトや配列がネストしているケースです。

DataFrame変換の基本方針

変換の方針は「ネストの深さ」で決まります。

ネストの深さ 推奨する変換方法 対象関数
1階層(フラット) そのまま変換 pd.DataFrame()
2階層(辞書の中に辞書) 正規化して展開 pd.json_normalize()
3階層以上(深いネスト) 再帰的に展開 pd.json_normalize() + カスタム関数

この判断基準を覚えておけば、どんなJSONが来ても迷いません。

【コピペOK】JSON→DataFrame変換の5パターン

まず必要なライブラリをインストールしてください。


pip install pandas requests

以下がメインコードです。


import json
import pandas as pd
import requests
from typing import Any, Dict, List, Optional

# ==============================
# 設定エリア
# ==============================
SAMPLE_API_URL: str = "https://jsonplaceholder.typicode.com/posts"
REQUEST_TIMEOUT: int = 10

# ==============================
# パターン1: フラットなJSONリスト → DataFrame
# ==============================
def pattern1_flat_list() -> pd.DataFrame:
    'ネストのないJSONリストをそのまま変換する'
    raw_json: str = '"
    [
        {"date": "2024-01-02", "open": 2510, "high": 2530, "low": 2500, "close": 2520, "volume": 1200000},
        {"date": "2024-01-03", "open": 2525, "high": 2550, "low": 2515, "close": 2545, "volume": 1350000},
        {"date": "2024-01-04", "open": 2540, "high": 2560, "low": 2530, "close": 2555, "volume": 1100000}
    ]
    '"
    data: List[Dict[str, Any]] = json.loads(raw_json)
    df: pd.DataFrame = pd.DataFrame(data)
    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date")
    print("=== パターン1: フラットなリスト ===")
    print(df)
    print()
    return df

# ==============================
# パターン2: ネストされた辞書 → json_normalize
# ==============================
def pattern2_nested_dict() -> pd.DataFrame:
    'オブジェクト内にネストした辞書を展開する'
    raw_json: str = '"
    [
        {
            "symbol": "7203.T",
            "date": "2024-01-02",
            "price": {"open": 2510, "high": 2530, "low": 2500, "close": 2520},
            "volume": 1200000
        },
        {
            "symbol": "7203.T",
            "date": "2024-01-03",
            "price": {"open": 2525, "high": 2550, "low": 2515, "close": 2545},
            "volume": 1350000
        }
    ]
    '"
    data: List[Dict[str, Any]] = json.loads(raw_json)
    df: pd.DataFrame = pd.json_normalize(data)
    df.columns = [c.replace("price.", ') for c in df.columns]
    print("=== パターン2: ネストされた辞書 ===")
    print(df)
    print()
    return df

# ==============================
# パターン3: メタデータ + データ本体の分離構造
# ==============================
def pattern3_meta_and_data() -> pd.DataFrame:
    'ルートにメタ情報とデータ配列が分離したJSONを処理する'
    raw_json: str = '"
    {
        "meta": {"symbol": "7203.T", "currency": "JPY", "timezone": "Asia/Tokyo"},
        "data": [
            {"date": "2024-01-02", "close": 2520, "volume": 1200000},
            {"date": "2024-01-03", "close": 2545, "volume": 1350000},
            {"date": "2024-01-04", "close": 2555, "volume": 1100000}
        ]
    }
    '"
    parsed: Dict[str, Any] = json.loads(raw_json)
    meta: Dict[str, str] = parsed["meta"]
    df: pd.DataFrame = pd.DataFrame(parsed["data"])

    for key, value in meta.items():
        df[key] = value

    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date")
    print("=== パターン3: メタデータ + データ本体 ===")
    print(df)
    print()
    return df

# ==============================
# パターン4: ネストされた配列(リスト内リスト)
# ==============================
def pattern4_nested_array() -> pd.DataFrame:
    '値がリストで返されるOHLCVデータを変換する'
    raw_json: str = '"
    {
        "symbol": "AAPL",
        "candles": [
            [1704153600, 185.5, 186.2, 184.8, 185.9, 5000000],
            [1704240000, 186.0, 187.5, 185.5, 187.0, 5200000],
            [1704326400, 187.2, 188.0, 186.0, 186.5, 4800000]
        ]
    }
    '"
    parsed: Dict[str, Any] = json.loads(raw_json)
    column_names: List[str] = ["timestamp", "open", "high", "low", "close", "volume"]
    df: pd.DataFrame = pd.DataFrame(parsed["candles"], columns=column_names)
    df["date"] = pd.to_datetime(df["timestamp"], unit="s")
    df = df.drop(columns=["timestamp"]).set_index("date")
    df["symbol"] = parsed["symbol"]
    print("=== パターン4: ネストされた配列 ===")
    print(df)
    print()
    return df

# ==============================
# パターン5: 深いネスト → 再帰的フラット化
# ==============================
def flatten_dict(
    d: Dict[str, Any], parent_key: str = ', sep: str = "_",
) -> Dict[str, Any]:
    'ネストされた辞書を再帰的にフラット化する'
    items: list = []
    for k, v in d.items():
        new_key: str = f"{parent_key}{sep}{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(flatten_dict(v, new_key, sep).items())
        elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
            for idx, item in enumerate(v):
                items.extend(flatten_dict(item, f"{new_key}_{idx}", sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

def pattern5_deep_nest() -> pd.DataFrame:
    '3階層以上のネストを再帰的に展開する'
    raw_json: str = '"
    [
        {
            "symbol": "7203.T",
            "quote": {
                "price": {"current": 2520, "change": 15},
                "volume": {"today": 1200000, "avg_30d": 1500000}
            },
            "fundamentals": {"per": 10.5, "pbr": 1.2}
        },
        {
            "symbol": "6758.T",
            "quote": {
                "price": {"current": 13500, "change": -200},
                "volume": {"today": 800000, "avg_30d": 900000}
            },
            "fundamentals": {"per": 15.3, "pbr": 2.1}
        }
    ]
    '"
    data: List[Dict[str, Any]] = json.loads(raw_json)
    flat_data: List[Dict[str, Any]] = [flatten_dict(record) for record in data]
    df: pd.DataFrame = pd.DataFrame(flat_data)
    print("=== パターン5: 深いネスト(再帰展開) ===")
    print(df)
    print()
    return df

# ==============================
# 実際のAPIからの取得デモ
# ==============================
def fetch_real_api(url: str, timeout: int) -> Optional[pd.DataFrame]:
    '実際のWeb APIからJSONを取得してDataFrameに変換する'
    try:
        response: requests.Response = requests.get(url, timeout=timeout)
        response.raise_for_status()
        data: Any = response.json()

        if isinstance(data, list):
            df: pd.DataFrame = pd.json_normalize(data)
        elif isinstance(data, dict):
            key: str = next(
                (k for k, v in data.items() if isinstance(v, list)), ',
            )
            if key:
                df = pd.json_normalize(data[key])
            else:
                df = pd.json_normalize(data)
        else:
            print("予期しないJSON構造です。")
            return None

        print(f"=== API取得結果 ({url}) ===")
        print(f"行数: {len(df)}, 列数: {len(df.columns)}")
        print(df.head())
        print()
        return df

    except requests.RequestException as e:
        print(f"API取得失敗: {e}")
        return None

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    df1: pd.DataFrame = pattern1_flat_list()
    df2: pd.DataFrame = pattern2_nested_dict()
    df3: pd.DataFrame = pattern3_meta_and_data()
    df4: pd.DataFrame = pattern4_nested_array()
    df5: pd.DataFrame = pattern5_deep_nest()
    df_api: Optional[pd.DataFrame] = fetch_real_api(SAMPLE_API_URL, REQUEST_TIMEOUT)

コードの処理フロー解説

上記のコードは、以下の6ステップで構成されています。

* ステップ1(パターン1):フラットなJSONリストをjson.loadsでパースし、pd.DataFrame()にそのまま渡す。最もシンプルなケース

* ステップ2(パターン2):辞書の中に辞書がネストしたJSONをpd.json_normalize()で自動展開する。カラム名はドット区切りで生成されるため、replaceで整形する

* ステップ3(パターン3):ルート直下にメタ情報とデータ配列が分離した構造を処理する。parsed["data"]でデータ部分だけを取り出し、メタ情報を列として追加する

* ステップ4(パターン4):OHLCVがリストのリストで返される構造を処理する。カラム名を手動で指定し、UNIXタイムスタンプを日時に変換する

* ステップ5(パターン5):3階層以上のネストをflatten_dict関数で再帰的にフラット化する。キー名はアンダースコア区切りで結合される

* ステップ6(API取得デモ):実際のWeb APIからJSONを取得し、レスポンスの型(リストか辞書か)に応じて自動的に変換ルートを分岐する

fetch_real_api関数のURL部分を自分が使う金融APIのエンドポイントに差し替えれば、そのまま実用できます。

JSONパターンの見分け方と実践テクニック

APIレスポンスを最初に確認する手順

見知らぬAPIのレスポンスを受け取ったら、まず構造を確認してください。以下の2行で十分です。


import json
print(json.dumps(data, indent=2, ensure_ascii=False)[:2000])

先頭2,000文字だけ整形表示すれば、ネストの深さとパターンを判別できます。巨大なレスポンスを全文表示すると端末が固まる場合があるので、必ずスライスで制限してください。

json_normalizeの主要パラメータ

pd.json_normalize()にはネスト展開を制御するパラメータがあります。

パラメータ 役割 使用例
record_path 展開対象のネスト配列のキーを指定 record_path="trades"
meta 親階層から引き継ぐキーを指定 meta=["symbol", "date"]
sep 展開後のカラム名の区切り文字 sep="_"(デフォルトは.
max_level 展開する最大階層数 max_level=1

record_pathmetaの組み合わせを覚えておくだけで、金融APIの大半のレスポンス構造に対応できます。

【コピペOK】複数ページのJSON取得とDataFrame結合

金融APIはページネーション(Pagination)で結果を分割して返すことがあります。以下のコードは複数ページを自動取得してDataFrameに結合する汎用関数です。


import time
import pandas as pd
import requests
from typing import Any, Dict, List, Optional

# ==============================
# 設定エリア
# ==============================
BASE_URL: str = "https://jsonplaceholder.typicode.com/posts"
MAX_PAGES: int = 3
PER_PAGE: int = 10
REQUEST_INTERVAL: float = 1.0
REQUEST_TIMEOUT: int = 10

# ==============================
# ページネーション付きJSON取得
# ==============================
def fetch_paginated_json(
    base_url: str,
    max_pages: int,
    per_page: int,
    interval: float,
    timeout: int,
) -> pd.DataFrame:
    '複数ページのJSONを取得してDataFrameに結合する'
    all_records: List[Dict[str, Any]] = []

    for page in range(1, max_pages + 1):
        params: Dict[str, Any] = {"_page": page, "_limit": per_page}
        try:
            response: requests.Response = requests.get(
                base_url, params=params, timeout=timeout,
            )
            response.raise_for_status()
            data: Any = response.json()

            if not data:
                print(f"  ページ {page}: データなし → 終了")
                break

            all_records.extend(data if isinstance(data, list) else [data])
            print(f"  ページ {page}: {len(data)}件 取得")

        except requests.RequestException as e:
            print(f"  ページ {page}: 取得失敗 - {e}")
            break

        if page < max_pages:
            time.sleep(interval)

    df: pd.DataFrame = pd.json_normalize(all_records)
    print(f"n合計 {len(df)}件 を結合しました。")
    return df

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    print("--- ページネーション取得開始 ---")
    df_pages: pd.DataFrame = fetch_paginated_json(
        BASE_URL, MAX_PAGES, PER_PAGE, REQUEST_INTERVAL, REQUEST_TIMEOUT,
    )
    print(df_pages.head(10))

コードの処理フロー解説

上記のコードは、以下の3ステップで構成されています。

* ステップ1(ループ取得):ページ番号をインクリメントしながらAPIを呼び出し、レスポンスが空になるか最大ページ数に達するまで繰り返す

* ステップ2(リスト蓄積):各ページのJSONレコードをall_recordsリストにextendで追加する。リクエスト間にはtime.sleepでインターバルを入れ、レートリミット(Rate Limit)を回避する

* ステップ3(一括変換):蓄積した全レコードをpd.json_normalizeで一括変換し、1つのDataFrameとして返す

BASE_URLとクエリパラメータの形式を自分のAPIに合わせれば、任意のページネーションAPIに対応できます。

よくあるエラーと対処法

JSONDecodeError: Expecting value

APIレスポンスがJSON形式ではない(HTMLエラーページや空文字列が返っている)場合に発生します。認証エラーやURLの間違いが主な原因です。

以下を試してください。

* response.text[:500]printして、実際のレスポンス内容を確認する

* APIキーやアクセストークンが正しく設定されているか確認する

* URLのエンドポイントにタイプミスがないか確認する

KeyError でネスト内のキーが見つからない

JSONの構造がAPI側で変更された、またはレコードによってキーの有無が異なる場合に発生します。金融APIではフィールドが欠落するケースが珍しくありません。

以下を試してください。

* dict.get("key", default_value)を使い、キーが存在しない場合のデフォルト値を設定する

* pd.json_normalizeerrors="ignore"パラメータを利用する

* 変換前にjson.dumps(data[0], indent=2)で先頭レコードの構造を確認する

DataFrameの列に辞書やリストがそのまま残る

pd.DataFrame()はネスト1階層までしか自動展開しません。2階層以上のネストが残っている状態です。

pd.json_normalize()に切り替えてください。それでも残る場合は、本記事のパターン5で紹介したflatten_dict関数を適用することで、任意の深さのネストをフラット化できます。max_levelパラメータで展開階層を制御するとカラム数の爆発を防げます。

まとめ

この記事では、PythonでAPIから取得したJSONデータをpandasのDataFrameに変換する5つのパターンと実践テクニックを解説しました。

要点を整理します。

* フラットなJSONリストはpd.DataFrame()に直接渡すだけで変換できる

* ネストした辞書はpd.json_normalize()で自動展開し、record_pathmetaパラメータでネスト配列と親情報を制御する

* 3階層以上の深いネストには再帰的フラット化関数(flatten_dict)を使い、すべてのキーをアンダースコア区切りで1階層に展開する

* APIレスポンスを受け取ったら、まずjson.dumpsで先頭2,000文字を整形表示し、構造を目視確認する習慣をつける

* ページネーション付きAPIはtime.sleepでインターバルを入れながらループ取得し、json_normalizeで一括変換する

次のステップとして、取得・変換したDataFrameをCSVやSQLite(軽量データベース)に保存し、ローカルキャッシュを構築することを推奨します。API呼び出しの回数を減らせるだけでなく、オフラインでの分析も可能になります。

さらに、本記事のfetch_real_api関数とfetch_with_fallbackパターン(複数データソースの自動切り替え)を組み合わせれば、どのAPIが落ちてもデータ取得が止まらない堅牢なパイプラインを構築できます。

タイトルとURLをコピーしました