PandasとJSONの連携:データ分析の基礎を紹介
はじめに
現代のデータ分析において、様々な形式のデータを効率的に処理できる能力は不可欠です。その中でも、PandasとJSONは特に重要なツールとデータフォーマットの組み合わせと言えるでしょう。
データは多様なソースからやってきます。リレーショナルデータベース、CSVファイル、Excelスプレッドシートはもちろんのこと、Web APIからのレスポンス、設定ファイル、ログデータなど、様々な場面でJSON形式のデータが登場します。一方、Pythonのデータ分析ライブラリであるPandasは、構造化データ(表形式データ)を扱うためのデファクトスタンダードとして広く利用されています。PandasのDataFrameは、データの読み込み、クリーニング、変換、集計、可視化など、データ分析ワークフローのほぼ全ての段階で中心的な役割を果たします。
したがって、JSON形式で提供されるデータをPandasで読み込み、分析可能な形に変換し、さらに分析結果をJSONとして出力するスキルは、データサイエンティストやデータエンジニア、ソフトウェア開発者にとって必須のものとなっています。
本記事では、PandasとJSONの連携に焦点を当て、その基本的な使い方から、ネストした複雑なJSON構造の扱い、さらには応用的な活用方法までを詳細に解説します。約5000語にわたる網羅的な解説を通じて、読者の皆様がJSONデータを自信を持ってPandasで扱えるようになることを目指します。
具体的な内容として、まずはJSONとPandasそれぞれの基礎を簡単に紹介し、次にPandasを使ったJSONデータの読み込み(pd.read_json)と、DataFrameのJSON形式での書き出し(df.to_json)について、様々なorientパラメータの意味と使い方を豊富なコード例とともに掘り下げます。さらに、JSONデータで頻繁に登場するネストした構造をフラット化するための強力なツールであるpd.json_normalizeの使い方を詳しく解説します。最後に、API連携、エラー処理、パフォーマンス考慮、実世界のシナリオなど、応用的な内容にも触れ、PandasとJSON連携に関する包括的な知識を提供します。
それでは、データ分析における強力な武器となるPandasとJSONの連携の世界に入っていきましょう。
JSONとは何か?
JSON (JavaScript Object Notation) は、軽量なデータ交換フォーマットです。人間が読み書きしやすく、機械が解析しやすいという特徴を持ちます。もともとはJavaScriptのサブセットでしたが、現在では多くのプログラミング言語でサポートされており、Web APIや設定ファイルなど、幅広い用途で利用されています。
JSONの基本的な構造要素は以下の2つです。
-
オブジェクト (Object): 波括弧
{}で囲まれ、キーと値のペアの集まりを表します。キーは文字列で、値は後述するいずれかの型になります。キーと値の間はコロン:で区切り、ペア同士はカンマ,で区切ります。リレーショナルデータベースのレコードや、プログラム言語における辞書(Dictionary)やハッシュマップ(Hash Map)に似ています。
例:{"name": "Alice", "age": 30} -
配列 (Array): 角括弧
[]で囲まれ、順序付けられた値のリストを表します。値同士はカンマ,で区切ります。リレーショナルデータベースのテーブルや、プログラム言語におけるリスト(List)や配列(Array)に似ています。
例:["apple", "banana", "cherry"]
値として使用できるデータ型は以下の通りです。
- 文字列 (String): ダブルクォーテーション
"で囲まれた Unicode 文字のシーケンスです。
例:"Hello, world!" - 数値 (Number): 整数または浮動小数点数です。
例:123,3.14 - 真偽値 (Boolean):
trueまたはfalseです。 - null: 値がないことを表します。
例:null - オブジェクト (Object): 前述のオブジェクトです。これにより、JSONはネストした構造を持つことができます。
例:{"address": {"city": "Tokyo", "zip": "100-0001"}} - 配列 (Array): 前述の配列です。これにより、配列の中に配列やオブジェクトを持つことができます。
例:[{"id": 1}, {"id": 2}]
JSONの構造は比較的シンプルですが、これらの要素を組み合わせることで、非常に複雑なデータ構造を表現することが可能です。特に、オブジェクトや配列を値として持つことができる「ネスト」の機能は、リレーショナルデータベースのようなフラットな構造では表現しにくい階層的なデータを表現するのに役立ちますが、Pandasで扱う際には特別な注意が必要になります。
JSONがデータ交換フォーマットとして広く普及している主な理由は以下の通りです。
- 軽量: XMLなどに比べて冗長性が少なく、ファイルサイズが小さくなります。
- 人間が読める: 構造がシンプルで、テキストエディタなどで内容を確認しやすいです。
- 機械が解析しやすい: 厳密な文法定義があり、多くのプログラミング言語で高速なパースライブラリが提供されています。
- 言語非依存: 特定のプログラミング言語に依存せず、異なるシステム間でデータを交換するのに適しています。
これらの特徴から、JSONはWeb APIのデータ形式、設定ファイル、ログファイル、データストア(MongoDBなどのNoSQLデータベース)など、様々な場面で利用されています。
Pandasとは何か?
Pandas (Python Data Analysis Library) は、Pythonでデータ分析を効率的に行うためのオープンソースライブラリです。特に、表形式データ(スプレッドシートやデータベースのテーブルのようなデータ)の操作に優れています。PandasはWes McKinney氏によって開発が開始され、現在では多くの貢献者によって活発に開発が進められています。
Pandasの主要なデータ構造は以下の2つです。
-
Series: 1次元のラベル付き配列です。数値、文字列、Pythonオブジェクトなど、様々な型のデータを格納できます。各要素にはインデックス(ラベル)が付けられます。
例:pd.Series([10, 20, 30], index=['a', 'b', 'c']) -
DataFrame: 2次元のラベル付きデータ構造で、異なる型の列を持つことができます。スプレッドシートやデータベースのテーブルと非常によく似ています。各列(Series)には名前(列名)が付けられ、各行にはインデックスが付けられます。DataFrameは、複数のSeriesを同じインデックスで結合したようなものと考えることもできます。
例:
python
import pandas as pd
data = {'col1': [1, 2], 'col2': [3, 4]}
df = pd.DataFrame(data, index=['row1', 'row2'])
Pandas DataFrameは、データ分析における多くの操作を効率的に行うための豊富な機能を提供します。
- データの読み込み・書き出し: CSV, Excel, SQLデータベース, JSON, HTMLなど、様々な形式のデータを読み書きできます。
- データのクリーニング: 欠損値の処理、重複データの削除、データ型の変換、不正なデータの修正など。
- データの選択・フィルタリング: 特定の列や行を選択したり、条件に基づいて行を絞り込んだりできます。
- データの操作: 列の追加・削除・名前変更、インデックスの操作など。
- データの結合・マージ: 複数のDataFrameを結合したり、マージしたりできます。
- データの集計・変換: グルーピング、ピボットテーブル、クロス集計など。
- 時系列データの処理: 日付・時刻データの扱い、リサンプリングなど。
- 統計分析: 基本的な統計量の計算。
- 可視化との連携: MatplotlibやSeabornなどのライブラリと連携し、DataFrameから直接グラフを生成できます。
Pandasのこれらの機能により、データの前処理から探索的データ分析、モデリングのためのデータ準備までを効率的に行うことができます。Pythonエコシステムにおけるデータ分析のデファクトスタンダードとして、データサイエンス分野で広く利用されています。
JSON形式のデータがPandasのDataFrameのような表形式にうまくマッピングできる場合、その強力なデータ操作・分析機能をすぐに活用できます。しかし、JSONは柔軟な構造を持つため、特にネストした構造を持つ場合には、Pandasで扱う前に適切な変換が必要となることがあります。
PandasとJSONの基本的な連携
Pandasは、JSONデータの読み込みと書き出しのための専用の関数を提供しています。これらの関数を使うことで、Pythonの組み込みjsonモジュールを使うことなく、JSONデータを直接DataFrameと相互変換できます。
JSONデータの読み込み (pd.read_json)
pd.read_json() 関数は、JSONファイルまたはJSON文字列をPandas DataFrameに読み込むための主要な関数です。
基本構文:
python
pd.read_json(path_or_buf, orient=None, typ='frame', dtype=None, convert_axes=None,
convert_dates=True, keep_default_dates=True, numpy=False, precise_float=False,
date_unit=None, encoding=None, lines=False, chunksize=None, compression='infer',
storage_options=None)
重要な引数をいくつか見てみましょう。
path_or_buf: JSONファイルへのパス(文字列)またはファイルライクオブジェクト。orient: JSONデータの構造がどのようにDataFrameにマッピングされるかを指定します。これは最も重要な引数の一つであり、JSONデータの形式に合わせて適切に設定する必要があります。指定可能な値は'records','columns','index','split','values','table'です。デフォルトでは、入力形式から自動的に推測しようとしますが、明示的に指定することが推奨されます。typ: 読み込むデータ構造のタイプを指定します。デフォルトは'frame'でDataFrameを読み込みます。他に'series'も指定可能ですが、DataFrameが一般的です。lines: JSONデータが各行に一つの独立したJSONオブジェクトとして格納されている場合(JSON Lines形式)にTrueを設定します。デフォルトはFalseです。encoding: ファイルのエンコーディングを指定します(例:'utf-8')。dtype: 読み込むデータの各列のデータ型を辞書形式で指定できます。parse_dates: 日付として解析する列を指定します。デフォルトでは日付らしき文字列を自動で解析しようとします (keep_default_dates=True)。
orient パラメータの詳細
orient パラメータは、JSONデータの構造とDataFrameの間のマッピング方法を定義します。JSONデータの形式によって適切なorientを指定する必要があります。
-
orient='records':
これは最も一般的で直感的な形式です。JSONデータが、各要素がDataFrameの1行に対応するオブジェクトのリストである場合に使用します。各オブジェクトのキーがDataFrameの列名になります。
json
[
{"name": "Alice", "age": 30, "city": "New York"},
{"name": "Bob", "age": 25, "city": "London"},
{"name": "Charlie", "age": 35, "city": "Paris"}
]
これをorient='records'で読み込むと、以下のDataFrameになります。
name age city
0 Alice 30 New York
1 Bob 25 London
2 Charlie 35 Paris
インデックスはデフォルトの0, 1, 2になります。コード例:
“`python
import pandas as pd
import jsonjson_string_records = “””
[
{“name”: “Alice”, “age”: 30, “city”: “New York”},
{“name”: “Bob”, “age”: 25, “city”: “London”},
{“name”: “Charlie”, “age”: 35, “city”: “Paris”}
]
“””文字列から読み込む場合
df_records = pd.read_json(json_string_records, orient=’records’)
print(df_records)ファイルから読み込む場合 (例: data.jsonというファイルに上記のJSONを保存)
with open(‘data_records.json’, ‘w’) as f:
f.write(json_string_records)
df_records_file = pd.read_json(‘data_records.json’, orient=’records’)
print(df_records_file)
“`
-
orient='columns':
JSONデータが、各キーがDataFrameの1列に対応し、その値がリストであるオブジェクトである場合に使用します。キーが列名になり、リストの要素がその列の行データになります。
json
{
"name": ["Alice", "Bob", "Charlie"],
"age": [30, 25, 35],
"city": ["New York", "London", "Paris"]
}
これをorient='columns'で読み込むと、以下のDataFrameになります。
name age city
0 Alice 30 New York
1 Bob 25 London
2 Charlie 35 Paris
orient='records'と同じDataFrame構造になりますが、JSONの形式が異なります。コード例:
“`python
import pandas as pd
import jsonjson_string_columns = “””
{
“name”: [“Alice”, “Bob”, “Charlie”],
“age”: [30, 25, 35],
“city”: [“New York”, “London”, “Paris”]
}
“””
df_columns = pd.read_json(json_string_columns, orient=’columns’)
print(df_columns)
“` -
orient='index':
JSONデータが、各キーがDataFrameの1行(インデックス)に対応し、その値が列名と値のペアを持つオブジェクトである場合に使用します。
json
{
"row1": {"name": "Alice", "age": 30},
"row2": {"name": "Bob", "age": 25},
"row3": {"name": "Charlie", "age": 35}
}
これをorient='index'で読み込むと、以下のDataFrameになります。
name age
row1 Alice 30
row2 Bob 25
row3 Charlie 35
JSONのトップレベルのキーがDataFrameのインデックスになります。コード例:
“`python
import pandas as pd
import jsonjson_string_index = “””
{
“row1”: {“name”: “Alice”, “age”: 30},
“row2”: {“name”: “Bob”, “age”: 25},
“row3”: {“name”: “Charlie”, “age”: 35}
}
“””
df_index = pd.read_json(json_string_index, orient=’index’)
print(df_index)
“` -
orient='split':
JSONデータが、DataFrameのインデックス、列名、データの3つの部分を明示的に持つオブジェクトである場合に使用します。この形式は、DataFrameをJSONとしてシリアライズする際によく使われます(後述のto_jsonを参照)。
json
{
"columns": ["name", "age"],
"index": ["row1", "row2", "row3"],
"data": [
["Alice", 30],
["Bob", 25],
["Charlie", 35]
]
}
これをorient='split'で読み込むと、以下のDataFrameになります。
name age
row1 Alice 30
row2 Bob 25
row3 Charlie 35コード例:
“`python
import pandas as pd
import jsonjson_string_split = “””
{
“columns”: [“name”, “age”],
“index”: [“row1”, “row2”, “row3”],
“data”: [
[“Alice”, 30],
[“Bob”, 25],
[“Charlie”, 35]
]
}
“””
df_split = pd.read_json(json_string_split, orient=’split’)
print(df_split)
“` -
orient='values':
JSONデータが、DataFrameのデータ部分のみを持つリストのリストである場合に使用します。この場合、DataFrameにはデフォルトの整数インデックスと列名が付きます。
json
[
["Alice", 30],
["Bob", 25],
["Charlie", 35]
]
これをorient='values'で読み込むと、以下のDataFrameになります。
0 1
0 Alice 30
1 Bob 25
2 Charlie 35コード例:
“`python
import pandas as pd
import jsonjson_string_values = “””
[
[“Alice”, 30],
[“Bob”, 25],
[“Charlie”, 35]
]
“””
df_values = pd.read_json(json_string_values, orient=’values’)
print(df_values)
“` -
orient='table':
JSONデータが、Table Schema のデータリソース記述仕様に準拠している場合に使用します。これはメタデータ(列の型など)を含む形式です。
json
{
"schema": {
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "integer"}
],
"primaryKey": ["name"]
},
"data": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]
}
これをorient='table'で読み込むと、以下のDataFrameになります。スキーマ情報も利用される可能性があります。
name age
0 Alice 30
1 Bob 25
2 Charlie 35コード例:
“`python
import pandas as pd
import jsonjson_string_table = “””
{
“schema”: {
“fields”: [
{“name”: “name”, “type”: “string”},
{“name”: “age”, “type”: “integer”}
],
“primaryKey”: [“name”]
},
“data”: [
{“name”: “Alice”, “age”: 30},
{“name”: “Bob”, “age”: 25},
{“name”: “Charlie”, “age”: 35}
]
}
“””
df_table = pd.read_json(json_string_table, orient=’table’)
print(df_table)
“`
このように、orientパラメータはJSONデータの構造を正確に把握し、それに対応する値を指定することが重要です。最も頻繁に使用されるのは 'records' 形式です。
JSON Lines形式 (lines=True)
JSON Lines形式は、各行が独立したJSONオブジェクトであるテキストファイル形式です。これは、ログファイルやストリームデータなどでよく見られます。各行が改行文字 \n で区切られています。
例:
json
{"id": 1, "event": "login"}
{"id": 2, "event": "logout"}
{"id": 1, "event": "view_page"}
このような形式のファイルを読み込む場合は、lines=True を指定します。各行が独立したJSONオブジェクトであるため、orient='records' と組み合わせるのが一般的です。
コード例:
“`python
import pandas as pd
import json
from io import StringIO
json_lines_string = “””
{“id”: 1, “event”: “login”}
{“id”: 2, “event”: “logout”}
{“id”: 1, “event”: “view_page”}
“””
StringIOを使って文字列からファイルライクオブジェクトを作成し読み込む
df_lines_from_string = pd.read_json(StringIO(json_lines_string), orient=’records’, lines=True)
print(df_lines_from_string)
ファイルとして保存して読み込む場合 (例: events.jsonlというファイルに上記のJSONを保存)
with open(“events.jsonl”, “w”) as f:
f.write(json_lines_string.strip())
df_lines_file = pd.read_json(“events.jsonl”, orient=’records’, lines=True)
print(df_lines_file)
出力:
id event
0 1 login
1 2 logout
2 1 view_page
“`
その他の便利な引数
convert_dates=True:Trueの場合、日付らしき列を自動的に datetime オブジェクトに変換しようとします。特定の列を指定することも可能です (convert_dates=['col1', 'col2'])。dtype: 特定の列を強制的に特定のデータ型で読み込みたい場合に辞書形式で指定します (dtype={'col1': 'int', 'col2': 'float'})。chunksize: JSON Lines形式 (lines=True) の場合にのみ有効で、大規模なファイルを一度にメモリに読み込まず、チャンクごとに読み込む場合に指定します。これによりメモリ使用量を抑えられます。ファイル全体が単一のJSONオブジェクト/配列の場合は、chunksizeは使用できません。
read_json における一般的な問題と解決策
- JSON構文エラー: 最も一般的な問題です。JSONデータがRFC 8259に準拠していない場合に発生します(例: 末尾のカンマ、キーや文字列がダブルクォーテーションで囲まれていない、コメントが含まれているなど)。オンラインのJSONバリデーターなどでチェックし、エラーを修正する必要があります。
pd.read_json(errors='coerce')(非推奨、将来廃止される可能性あり)や、JSON Lines形式の場合はlines=Trueと組み合わせて使うことで不正な行をスキップできる場合もありますが、基本的にはデータソース側の修正が望ましいです。 orientの不一致: JSONデータの実際の構造と指定したorientが一致しない場合、エラーになったり、期待しないDataFrameが生成されたりします。JSONファイルの内容を確認し、適切なorientを指定し直してください。- エンコーディングの問題: ファイルのエンコーディングが指定したものと異なる場合に文字化けやエラーが発生します。ファイルの正しいエンコーディング(UTF-8が推奨されますが、他のエンコーディングの場合もあります)を指定してください。
- ネストした構造:
read_jsonは、基本的にはトップレベルまたは指定したorientに合うフラットな構造をDataFrameにマッピングします。ネストしたJSONオブジェクトや配列は、デフォルトではそのままDataFrameのセルに格納されることが多く、これらを分析可能なフラットな形にするには後述のjson_normalizeが必要になります。 - 大規模ファイルとメモリ: ファイルサイズが非常に大きい場合、メモリ不足になることがあります。JSON Lines形式の場合は
chunksizeを使用できます。単一の巨大なJSONオブジェクト/配列の場合は、ストリーミングパースが可能な別のライブラリ(ijsonなど)と組み合わせることを検討してください。
DataFrameからJSONへの書き出し (df.to_json)
Pandas DataFrameをJSON形式でファイルや文字列として書き出すには、DataFrameの .to_json() メソッドを使用します。
基本構文:
python
df.to_json(path_or_buf=None, orient=None, date_format='epoch', double_precision=10,
force_ascii=True, date_unit='ms', default_handler=None, lines=False,
compression='infer', index=True, indent=None, storage_options=None)
こちらもいくつかの重要な引数があります。
path_or_buf: 書き出すファイルパス(文字列)またはファイルライクオブジェクト。Noneの場合、JSON文字列を返します。orient: 生成するJSONデータの構造を指定します。read_jsonと同様の値を指定できます ('records','columns','index','split','values','table')。デフォルトは'columns'です。lines:Trueに設定すると、各行を独立したJSONオブジェクトとしてJSON Lines形式で書き出します。orient='records'と組み合わせるのが一般的です。indent: JSONにインデント(字下げ)を付けて書き出し、可読性を向上させます。整数でインデントレベルを指定します(例:indent=4)。lines=Trueの場合はインデントは無視されます。index:Trueの場合、DataFrameのインデックスもJSONに含めます。orientの値によっては無視されることもあります ('values','records')。date_format: 日付/時刻列をJSONでどのように表現するかを指定します。デフォルトは'epoch'(Unixタイムスタンプ)ですが、'iso'(ISO 8601形式の文字列)もよく使われます。
orient パラメータの詳細 (to_json)
to_json メソッドの orient パラメータも、生成されるJSONの構造を制御します。read_json の逆変換と考えることができます。
-
orient='records':
DataFrameの各行をJSONオブジェクトとし、それらのオブジェクトのリストを生成します。各オブジェクトのキーはDataFrameの列名になります。DataFrameのインデックスはデフォルトでは含まれません (index=Falseと同等)。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data)
print(df.to_json(orient=’records’, indent=2))
出力:json
[
{
“name”: “Alice”,
“age”: 30
},
{
“name”: “Bob”,
“age”: 25
}
]
“` -
orient='columns':
DataFrameの各列をキーとし、その列の値(行データ)をリストとしたオブジェクトを生成します。DataFrameのインデックスが、内部のリストのインデックスまたはキーとして含まれます。これはto_jsonのデフォルトです。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data, index=[‘a’, ‘b’])
print(df.to_json(orient=’columns’, indent=2))
出力:json
{
“name”: {
“a”: “Alice”,
“b”: “Bob”
},
“age”: {
“a”: 30,
“b”: 25
}
}
“` -
orient='index':
DataFrameの各インデックスをキーとし、その値が列名と値のペアを持つオブジェクトであるようなトップレベルのオブジェクトを生成します。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data, index=[‘a’, ‘b’])
print(df.to_json(orient=’index’, indent=2))
出力:json
{
“a”: {
“name”: “Alice”,
“age”: 30
},
“b”: {
“name”: “Bob”,
“age”: 25
}
}
“` -
orient='split':
DataFrameのインデックス、列名、データの3つの部分を明示的に含むオブジェクトを生成します。これは、DataFrameの構造を忠実に再現したい場合や、read_json(orient='split')と組み合わせて使う場合に便利です。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data, index=[‘a’, ‘b’])
print(df.to_json(orient=’split’, indent=2))
出力:json
{
“columns”: [
“name”,
“age”
],
“index”: [
“a”,
“b”
],
“data”: [
[
“Alice”,
30
],
[
“Bob”,
25
]
]
}
“` -
orient='values':
DataFrameのデータ部分のみをリストのリストとして生成します。インデックスや列名は含まれません。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data, index=[‘a’, ‘b’])
print(df.to_json(orient=’values’, indent=2))
出力:json
[
[
“Alice”,
30
],
[
“Bob”,
25
]
]
“` -
orient='table':
DataFrameを Table Schema 形式で書き出します。スキーマ情報も含まれます。
“`python
import pandas as pddata = {‘name’: [‘Alice’, ‘Bob’], ‘age’: [30, 25]}
df = pd.DataFrame(data)
print(df.to_json(orient=’table’, indent=2))
出力:json
{
“schema”: {
“fields”: [
{
“name”: “index”, # デフォルトインデックスが追加される
“type”: “integer”
},
{
“name”: “name”,
“type”: “string”
},
{
“name”: “age”,
“type”: “integer”
}
],
“primaryKey”: [ # index=Trueの場合、デフォルトインデックスがプライマリーキーに
“index”
],
“pandas_version”: “…” # Pandasのバージョンによっては出力が異なる場合があります
},
“data”: [
{
“index”: 0,
“name”: “Alice”,
“age”: 30
},
{
“index”: 1,
“name”: “Bob”,
“age”: 25
}
]
}
``index=True(デフォルト) の場合、デフォルトの整数インデックスが“index”という列として追加されます。インデックスをJSONに含めたくない場合はindex=False` を指定します。
JSON Lines形式での書き出し (lines=True)
lines=True を指定すると、DataFrameの各行が独立したJSONオブジェクトとして、改行区切りで書き出されます。これは orient='records' と組み合わせて使用するのが自然です。indent は lines=True の場合は無視されます。
“`python
import pandas as pd
from io import StringIO
data = {‘name’: [‘Alice’, ‘Bob’, ‘Charlie’], ‘age’: [30, 25, 35]}
df = pd.DataFrame(data)
文字列として取得する場合
StringIOはファイルライクオブジェクトとしてto_jsonに渡せるが、
lines=Trueで文字列として取得したい場合は、パスをNoneにする
json_lines_output = df.to_json(orient=’records’, lines=True, path_or_buf=None)
print(json_lines_output)
ファイルとして書き出す場合
df.to_json(“output_events.jsonl”, orient=’records’, lines=True)
出力:json
{“name”:”Alice”,”age”:30}
{“name”:”Bob”,”age”:25}
{“name”:”Charlie”,”age”:35}
“`
(実際には各オブジェクトの間に改行が入ります)
to_json メソッドも、read_json と同様に様々なオプションを提供しており、出力されるJSON形式を細かく制御できます。特にorientとlines、そしてindentは、JSONの用途(データ交換、設定ファイル、可読性重視など)に合わせて適切に使い分けることが重要です。
ネストしたJSONデータの取り扱い
JSONデータは、その柔軟性からしばしばネストした構造を持ちます。例えば、顧客データの中に複数の住所情報が配列として含まれていたり、注文データの中に複数の商品情報が配列として含まれていたりするケースです。
例:ユーザー情報とそれに紐づく複数の注文情報を含むJSON
json
[
{
"user_id": "user_001",
"name": "Alice",
"orders": [
{"order_id": "order_A1", "amount": 1000, "date": "2023-01-15"},
{"order_id": "order_A2", "amount": 2500, "date": "2023-02-20"}
]
},
{
"user_id": "user_002",
"name": "Bob",
"orders": [
{"order_id": "order_B1", "amount": 500, "date": "2023-03-01"}
]
}
]
このJSONを pd.read_json(orient='records') でそのまま読み込むと、DataFrameは以下のようになります。
user_id name orders
0 user_001 Alice [{'order_id': 'order_A1', 'amount': 1000, 'date...
1 user_002 Bob [{'order_id': 'order_B1', 'amount': 500, 'date'...
orders 列には、JSONオブジェクトのリストがPythonのリストとして格納されています。このままでは、個々の注文の詳細(order_id, amount, date)に対してフィルタリングや集計を行うことは困難です。データ分析のためには、これらのネストした情報をフラットな表形式に展開する必要があります。
Pandas 1.0から導入された pd.json_normalize() 関数は、このようなネストしたJSONデータをフラット化するための強力なツールです。
pd.json_normalize() の紹介
pd.json_normalize() は、JSONデータ(Pythonのリストや辞書)を受け取り、DataFrameを生成します。特に、ネストした構造を持つリストや辞書を、親要素の情報を保持しながらフラット化する機能に優れています。
基本構文:
python
pd.json_normalize(data, record_path=None, meta=None,
meta_prefix=None, sep='.', errors='raise', max_level=None)
重要な引数:
data: フラット化したいJSONデータ(Pythonのリストまたは辞書)。record_path: リストになっているネストした要素へのパスを指定します。例えば、上記の例では"orders"がこれに該当します。複数のネストレベルを辿る場合はリストでパスを指定します(例:['user', 'address', 'street_history'])。指定しない場合、dataのトップレベルがフラット化されます。meta: フラット化されたレコード(record_pathで指定した要素)に紐付けたい、親要素や他のトップレベルの要素へのパスを指定します。リストで複数の要素を指定できます。各要素は文字列(トップレベルのキー)またはタプル(ネストしたキーへのパス、例:('user', 'name'))で指定します。meta_prefix:metaで指定した列名に付与する接頭辞です。sep: ネストしたキー名をフラット化する際に使用する区切り文字です。デフォルトは.です(例:"address.city")。errors: パスが見つからない場合などのエラー処理方法。'raise'(例外発生),'ignore'(エラーを無視してNaNを埋める),'coerce'(エラー値をNaNに変換) が指定できます。max_level: フラット化するネストの最大深度を指定します。
json_normalize の実践例
上記のユーザーと注文の例を使って json_normalize を見てみましょう。
“`python
import pandas as pd
import json
json_data = “””
[
{
“user_id”: “user_001”,
“name”: “Alice”,
“orders”: [
{“order_id”: “order_A1”, “amount”: 1000, “date”: “2023-01-15”},
{“order_id”: “order_A2”, “amount”: 2500, “date”: “2023-02-20”}
]
},
{
“user_id”: “user_002”,
“name”: “Bob”,
“orders”: [
{“order_id”: “order_B1”, “amount”: 500, “date”: “2023-03-01”}
]
}
]
“””
data = json.loads(json_data) # JSON文字列をPythonのリストにパース
ネストした ‘orders’ リストをフラット化し、親の ‘user_id’ と ‘name’ をメタ情報として保持
df_normalized = pd.json_normalize(
data,
record_path=’orders’, # フラット化したいリストのキー
meta=[‘user_id’, ‘name’] # フラット化された各レコード(注文)に紐付けたい親要素のキー
)
print(df_normalized)
出力:
order_id amount date user_id name
0 order_A1 1000 2023-01-15 user_001 Alice
1 order_A2 2500 2023-02-20 user_001 Alice
2 order_B1 500 2023-03-01 user_002 Bob
``json_normalize
このように、元のJSONデータは2つのユーザーオブジェクトのリストでしたが、を使うことで、各注文が独立した行となり、それぞれの注文に紐づくユーザー情報(user_id,name`)が繰り返されて保持されています。これにより、注文ごとの分析(例: 注文金額の合計、ユーザーごとの注文数など)が容易になります。
複雑なネスト構造の処理
record_path と meta は、より複雑なネスト構造にも対応できます。例えば、以下のようなJSON構造を考えます。
json
[
{
"id": "item_1",
"details": {
"name": "Laptop",
"specs": {
"cpu": "Intel i7",
"ram_gb": 16
},
"features": [
{"type": "display", "value": "15 inch"},
{"type": "keyboard", "value": "backlit"}
]
},
"tags": ["electronics", "computer"]
},
{
"id": "item_2",
"details": {
"name": "Book",
"specs": {
"author": "J.K. Rowling",
"pages": 300
},
"features": [
{"type": "cover", "value": "hardcover"}
]
},
"tags": ["fiction", "fantasy"]
}
]
このJSONでは、details はオブジェクト、その中の specs もオブジェクト、features はオブジェクトのリスト、そして tags は文字列のリストです。
これをフラット化してみましょう。
– details.name, details.specs.cpu, details.specs.ram_gb などのネストしたオブジェクト内の情報をフラット化したい。
– details.features のリストを展開し、各フィーチャーが独立した行になるようにしたいが、元のアイテムの情報(id, details.name など)も紐付けたい。
– tags のようなリスト自体は列として保持したい場合もある(または、タグごとに別のDataFrameとして正規化したい場合もある)。
まず、トップレベルのリストをそのまま json_normalize に渡してみます。record_path を指定しない場合、トップレベルの各要素がDataFrameの行になり、ネストしたオブジェクトやリストはそのままセルに格納されます。
“`python
json_data_complex = “””
[
{
“id”: “item_1”,
“details”: {
“name”: “Laptop”,
“specs”: {
“cpu”: “Intel i7”,
“ram_gb”: 16
},
“features”: [
{“type”: “display”, “value”: “15 inch”},
{“type”: “keyboard”, “value”: “backlit”}
]
},
“tags”: [“electronics”, “computer”]
},
{
“id”: “item_2”,
“details”: {
“name”: “Book”,
“specs”: {
“author”: “J.K. Rowling”,
“pages”: 300
},
“features”: [
{“type”: “cover”, “value”: “hardcover”}
]
},
“tags”: [“fiction”, “fantasy”]
}
]
“””
data_complex = json.loads(json_data_complex)
record_pathを指定しない場合
df_complex_simple = pd.json_normalize(data_complex)
print(“— Simple Normalize —“)
print(df_complex_simple)
出力:
— Simple Normalize —
id details tags
0 item_1 {‘name’: ‘Laptop’, ‘specs’: {‘cpu’: ‘Intel i7… [electronics, computer]
1 item_2 {‘name’: ‘Book’, ‘specs’: {‘author’: ‘J.K. Row… [fiction, fantasy]
``details列とtags` 列はそのままネストした構造(Pythonオブジェクト)として格納されています。
次に、details オブジェクト内の情報をフラット化してみましょう。details 自体はリストではないので record_path には指定できません。しかし、meta にタプルでパスを指定することで、ネストしたオブジェクト内の値を列として取り出すことができます。
“`python
details内の情報をフラット化
df_complex_meta = pd.json_normalize(
data_complex,
meta=[
‘id’, # トップレベルのキー
(‘details’, ‘name’), # ネストしたオブジェクトへのパス
(‘details’, ‘specs’, ‘cpu’), # さらにネストしたオブジェクトへのパス
(‘details’, ‘specs’, ‘ram_gb’),
(‘details’, ‘specs’, ‘author’),
(‘details’, ‘specs’, ‘pages’)
],
record_path=None, # トップレベル自体をフラット化するのでrecord_pathは不要
errors=’ignore’ # パスが見つからない場合(例: item_2にcpuがない)はNaNを埋める
)
print(“\n— Normalize with Meta —“)
print(df_complex_meta)
出力:
— Normalize with Meta —
id details.name details.specs.cpu details.specs.ram_gb details.specs.author details.specs.pages
0 item_1 Laptop Intel i7 16 NaN NaN
1 item_2 Book NaN NaN J.K. Rowling 300
``metaにタプルでパスを指定することで、ネストしたオブジェクトの値をフラットな列として取り出せました。sep=’.’がデフォルトで使われているため、列名は親キー.子キー.孫キーのようになります。errors=’ignore’を指定したことで、存在しないパス(例: item_2のdetails.specs.cpu)はエラーにならずNaN` が埋められています。
次に、details オブジェクト内の features リストを展開してみましょう。features リストは details オブジェクトの中にネストしているので、record_path は ['details', 'features'] と指定します。このとき、meta にはフラット化されたフィーチャー情報に紐付けたい、親要素(アイテム)の情報を指定します。
“`python
details.features リストをフラット化
df_features = pd.json_normalize(
data_complex,
record_path=[‘details’, ‘features’], # 展開したいリストへのパス
meta=[
‘id’, # 親のアイテムIDをメタ情報として保持
(‘details’, ‘name’) # 親のアイテム名をメタ情報として保持
],
meta_prefix=’item_’ # メタ情報の列名に接頭辞を付ける
)
print(“\n— Features Normalized —“)
print(df_features)
出力:
— Features Normalized —
type value item_id item_details_name
0 display 15 inch item_1 Laptop
1 keyboard backlit item_1 Laptop
2 cover hardcover item_2 Book
``record_pathに[‘details’, ‘features’]と指定することで、featuresリストの各要素(オブジェクト)が新しい行として展開されました。metaにはidと(‘details’, ‘name’)を指定することで、展開された各フィーチャー行に、それを含むアイテムのIDと名前が紐付けられています。meta_prefix=’item_’により、メタ情報の列名に“item_”` が付与されています。
このように、json_normalize は、ネストした オブジェクトのリスト をフラット化し、その親情報を meta で紐付けるというパターンで非常に威力を発揮します。
tags のような単純な文字列リストをフラット化したい場合(例: 各タグが新しい行になる)、元のデータを json_normalize で処理した後に explode() を使うのがPandasらしい方法です。
“`python
元のデータからidとtags列だけを抽出してDataFrameを作成
df_tags_raw = pd.json_normalize(data_complex)[[‘id’, ‘tags’]]
tags列のリストを展開
df_tags_exploded = df_tags_raw.explode(‘tags’)
print(“\n— Tags Exploded —“)
print(df_tags_exploded)
出力:
— Tags Exploded —
id tags
0 item_1 electronics
0 item_1 computer
1 item_2 fiction
1 item_2 fantasy
``explode()` は、Seriesの要素がリストやタプルの場合に、その要素を新しい行に展開し、元のインデックスを繰り返します。これは、多対多の関係(例: アイテムとタグ)を表現するのに便利です。
json_normalize のワークフロー例
一般的なワークフローとしては、まず json モジュールや requests ライブラリなどを使ってJSONデータをPythonオブジェクト(リストや辞書)として取得します。次に、そのデータ構造を把握し、pd.json_normalize() を使って分析に適した形にフラット化します。
推奨ワークフロー: Pythonオブジェクト取得 -> json_normalize
“`python
import pandas as pd
import json
import requests # APIから取得する場合
1. JSONデータソースからPythonオブジェクトを取得
ファイルから:
with open(‘your_data.json’, ‘r’) as f:
data = json.load(f)
APIから:
response = requests.get(‘your_api_endpoint’)
response.raise_for_status()
data = response.json()
2. データ構造を把握し、json_normalizeを適用してフラット化
例: ネストしたリスト ‘items’ があり、親情報 ‘user_id’ と ‘timestamp’ を保持したい
df_normalized = pd.json_normalize(
data,
record_path=’items’,
meta=[‘user_id’, ‘timestamp’],
sep=’‘ # 例: 区切り文字を’‘にする
)
3. 必要に応じて、record_pathにならなかったトップレベルの他の要素もDataFrameにする
例: ユーザー全体の情報(アイテムリスト以外)を別のDataFrameにしたい
df_top_level = pd.json_normalize(
data,
meta=[‘user_id’, ‘timestamp’, ‘status’] # 必要なトップレベルのキー
).drop(columns=[‘items’]) # アイテムリストの列は不要ならドロップ
4. できたDataFrameに対してデータ分析や加工を行う
例: アイテムごとのデータとユーザーごとのデータを結合する
df_merged = pd.merge(df_normalized, df_top_level, on=’user_id’)
``record_path
このワークフローは、ネストしたJSONをPandasで扱う際に最も柔軟で制御しやすい方法です。データ構造を理解し、とmeta`を適切に指定することが鍵となります。
一方、pd.read_json を使って一度読み込んでから json_normalize を使う方法も考えられますが、read_json で読み込んだDataFrameの列がPythonオブジェクト(辞書やリスト)になっている場合、その列を直接 json_normalize に渡すことはできません。一度その列を取り出してPythonのリストに変換する必要があります。これは少し手間がかかるため、初めからPythonオブジェクトとして取得し json_normalize に渡す方法が推奨されることが多いです。ただし、JSON Lines形式で、各行の中にネストした構造がある場合は、read_json(lines=True) で読み込んだ後、特定の列に対して json_normalize を適用する(列の各要素をリストにして渡す)といった使い方も可能です。
応用的な連携
PandasとJSONの連携は、単にファイルの読み書きにとどまりません。API連携や特定のデータベースとの連携、さらにはエラーハンドリングやパフォーマンス考慮まで、データ分析の現場で役立つ様々な応用があります。
APIからのJSONデータの取得と処理
多くのWeb APIは、レスポンスとしてJSON形式のデータを返します。PythonでWeb APIからデータを取得するには、requests ライブラリがよく使われます。取得したJSONレスポンスをPandas DataFrameに変換することで、APIデータをすぐに分析ワークフローに乗せることができます。
手順:
requestsライブラリを使ってAPIにリクエストを送る。- レスポンスからJSONデータを取得する (
response.json())。これはPythonのリストまたは辞書になります。 - 取得したPythonオブジェクトを
pd.DataFrame()またはpd.json_normalize()に渡してDataFrameを生成する。
コード例: 公開API (Open Notify – ISS Current Location) の利用
“`python
import requests
import pandas as pd
import json # 整形表示用
APIのエンドポイント
api_url = “http://api.open-notify.org/iss-now.json”
try:
# APIにGETリクエストを送信
response = requests.get(api_url)
# レスポンスが成功したか確認 (ステータスコード200番台)
response.raise_for_status() # HTTPエラーの場合は例外を発生させる
# レスポンスボディをJSONとしてパース
# これにより、Pythonの辞書またはリストが得られる
json_data = response.json()
print("--- Raw JSON Data from API ---")
print(json.dumps(json_data, indent=2)) # パース結果を表示 (整形)
# JSONデータ構造を確認
# {"iss_position": {"latitude": "...", "longitude": "..."}, "timestamp": ..., "message": "..."}
# これは単一のオブジェクト。
# 'iss_position' がネストしたオブジェクトになっている。
# json_normalizeを使ってフラット化
# json_normalizeはリストを受け取るのが一般的だが、単一のオブジェクトも処理できる。
# トップレベルの要素をmetaで、ネストしたiss_position内の要素もmetaにタプルパスで指定する。
df_api_normalized = pd.json_normalize(
json_data, # 単一のオブジェクト
meta=['timestamp', 'message',
('iss_position', 'latitude'), # ネストしたパスはタプルで指定
('iss_position', 'longitude')]
# record_path=None # トップレベルのオブジェクト自体をフラット化するのでrecord_pathは不要
)
print("\n--- DataFrame (Normalized with json_normalize) ---")
print(df_api_normalized)
# デフォルトのsep='.'によりカラム名が自動でネストに合わせて生成されていることを確認
print("\n--- DataFrame Columns ---")
print(df_api_normalized.columns)
# 必要に応じて日付列を変換
# timestampはUnixタイムスタンプ (秒単位) なのでunit='s'を指定
df_api_normalized['timestamp'] = pd.to_datetime(df_api_normalized['timestamp'], unit='s')
# 緯度・経度を数値型に変換
df_api_normalized['iss_position.latitude'] = pd.to_numeric(df_api_normalized['iss_position.latitude'])
df_api_normalized['iss_position.longitude'] = pd.to_numeric(df_api_normalized['iss_position.longitude'])
print("\n--- DataFrame (with Correct Types) ---")
print(df_api_normalized)
except requests.exceptions.RequestException as e:
print(f”APIリクエストエラー: {e}”)
except json.JSONDecodeError as e:
print(f”JSONパースエラー: {e}”)
except Exception as e:
print(f”その他のエラー: {e}”)
``response.json()
この例では、ISSの位置情報を取得するAPIからJSONデータを受け取り、でPythonの辞書に変換し、さらにpd.json_normalize()を使ってネストした位置情報(iss_position)内の緯度・経度を含む、全体のデータをフラット化してDataFrameにしています。metaにトップレベルのキーとネストしたパスをタプルで指定することで、これらの情報がDataFrameの列として含まれるようにしています。取得したデータ型が文字列になっている場合があるので、pd.to_datetimeやpd.to_numeric` で適切な型に変換する後処理も重要です。
API連携はデータ分析の重要なソースであり、JSONレスポンスをPandasで効率的に処理できることは、リアルタイムデータ分析やデータパイプライン構築において非常に価値のあるスキルです。
データベースとの連携(JSON型)
近年、PostgreSQLやMySQL、MongoDBなどのデータベースは、JSON型カラムをサポートするようになっています。これにより、スキーマが固定されていない柔軟なデータをデータベースに格納することが可能になりました。Pandasは、このようなデータベースからJSON型カラムを含むデータを読み込んだり、DataFrameをJSON型としてデータベースに書き込んだりする場合にも連携が可能です。
- 読み込み: データベースからSELECTクエリで取得したデータにJSON型カラムが含まれている場合、データベースコネクタ(
psycopg2,mysql.connector,pymongoなど)やSQLライブラリ(SQLAlchemyなど)を使ってデータを取得し、Pandasのread_sqlなどでDataFrameに読み込むと、JSON型カラムの内容は通常、Pythonの辞書やリストとしてDataFrameのセルに格納されます。その後は、前述のjson_normalizeなどを使って必要に応じてフラット化します。 - 書き出し: Pandas DataFrameをデータベースに書き込む際に、一部の列をJSON型として格納したい場合があります。DataFrameの該当列が既にPythonの辞書やリストの形式であれば、そのままデータベースのJSON型カラムに書き込めることが多いです(使用するドライバやライブラリに依存します)。そうでない場合は、
df.to_json(orient='records')などを使ってJSON文字列に変換してから書き込む必要がありますが、Pandas DataFrameのセルに直接JSON文字列を格納し、それをJSON型カラムに挿入するのが一般的です。SQLAlchemyなどを使うと、Pandasとデータベース間の型マッピングを自動的に処理してくれる場合があります。
例: DataFrameの列をJSON文字列に変換してDBに書き込む(概念)
“`python
import pandas as pd
import sqlalchemy as sa # SQLAlchemyを使う場合
data = {‘id’: [1, 2], ‘info’: [{‘city’: ‘Tokyo’, ‘zip’: ‘100’}, {‘city’: ‘Osaka’, ‘zip’: ‘500’}]}
df = pd.DataFrame(data)
‘info’列のPython辞書をJSON文字列に変換
apply(json.dumps) を使う
import json
df[‘info_json’] = df[‘info’].apply(json.dumps)
print(df)
id info info_json
0 1 {‘city’: ‘Tokyo’, ‘zip’: ‘100’} {“city”: “Tokyo”, “zip”: “100”}
1 2 {‘city’: ‘Osaka’, ‘zip’: ‘500’} {“city”: “Osaka”, “zip”: “500”}
df_cleaned = df.drop(columns=[‘info’]) # 不要になった元の列を削除
df_cleaned.to_sql(…) # データベースに書き出す (info_json列がJSON型カラムに対応)
“`
この領域は使用するデータベースやライブラリによって詳細が異なるため、ここでは概要に留めます。重要なのは、Pandas DataFrameとデータベースのJSON型カラムの間で、Pythonの辞書/リストとJSONデータ間の変換が自然に行われることを理解することです。
JSONデータのエラーと欠損値の処理
現実世界のJSONデータは、常にクリーンであるとは限りません。構文エラー、期待しない構造、欠損値などが含まれている可能性があります。
- JSON構文エラー:
pd.read_jsonやjson.loads()を使う際に、JSONデータ自体に文法的な誤りがある場合に発生します(json.JSONDecodeErrorなど)。これは、元のデータソースを確認して修正するか、エラーのある行をスキップするなどの前処理が必要です。JSON Lines形式であれば、pd.read_json(lines=True, errors='coerce')(Pandas 2.0以降でサポート)のようにerrors='coerce'引数で不正な行を無視し、NaNを埋めることができます。それ以外の形式では、手動でファイルを行ごとに読み込み、json.loads()をtry-exceptブロックで囲んでエラー行を特定・スキップするなどの処理が必要になります。 - 構造の不一致:
pd.read_jsonのorientやpd.json_normalizeのrecord_path/metaがJSONデータの実際の構造と一致しない場合に、エラーが発生したり、データが正しく読み込めなかったりします。JSONデータのサンプルを確認し、適切なパスやorientを指定し直す必要があります。json_normalizeのerrors='ignore'や'coerce'は、特定のキーが存在しない場合の対応に役立ちます。 - 欠損値 (Missing Values): JSONデータには
null値が含まれることがあります。Pandasはnullを通常NaN(Not a Number) として読み込みます(文字列型の場合はNoneや'null'となることもあります)。json_normalizeでフラット化した際も、特定のレコードにネストしたキーが存在しない場合はNaNが生成されます。これらの欠損値は、df.isnull(),df.notnull(),df.fillna(),df.dropna()などの標準的なPandas機能を使って処理します。 - データ型の不一致: JSONはスキーマレスな特性を持つため、同じキーでもレコードによって値の型が異なる場合があります(例: あるレコードでは数値、別のレコードでは文字列)。
pd.read_jsonは可能な限り適切な型に推測しますが、混在している場合はobject型として読み込まれることがあります。分析前にdf.astype()やpd.to_numeric(),pd.to_datetime()などを使って適切な型に変換する必要があります。変換できない値はエラーになるため、事前にデータの分布を確認し、必要であればクレンジングや型変換時のエラーハンドリング(例:pd.to_numeric(df['col'], errors='coerce')は変換できない値をNaNにします)を行います。
データの品質問題に対処することは、データ分析パイプラインの重要な一部です。JSONデータの場合、その柔軟性ゆえに様々な問題が発生する可能性があるため、注意深い確認と適切なエラー処理・データクリーニングが必要です。
パフォーマンスに関する考慮事項
大規模なJSONファイルを扱う場合、メモリ使用量や処理速度が問題になることがあります。Pandasの read_json は、デフォルトではファイル全体をメモリに読み込んでからパースするため、非常に大きなファイルではメモリ不足を引き起こす可能性があります。
-
大規模JSONファイルの読み込み:
- JSON Lines形式 (
lines=True) の場合:pd.read_jsonは内部的に行ごとに処理を行うため、メモリ効率が良い傾向があります。大規模なログデータなど、JSON Lines形式であればそのままread_jsonを使うのが良いでしょう。chunksize引数も利用できます。 - 単一の巨大なJSONオブジェクト/配列の場合:
read_jsonのchunksize引数は、ファイル全体が単一のJSON構造である場合には適用されません。この場合、Python標準のjsonモジュールや、ストリーミングパースに特化したライブラリ (ijsonなど) を使って、JSONデータをチャンクごとに(またはイベントドリブンで)パースし、得られたデータ構造をPandas DataFrameに追加していくという方法が考えられます。これはより複雑なコーディングが必要になりますが、メモリ効率は高まります。 - ネストが深い/要素数が多いJSONの
json_normalize:json_normalizeは便利な反面、展開される行数や列数が爆発的に増える可能性があるため、メモリを多く消費する可能性があります。必要な部分だけを抽出したり、事前にデータをフィルタリングしたり、メモリが許す範囲でチャンク処理(元のPythonリストを分割してjson_normalizeを複数回適用し、後でpd.concatするなど)を行うなどの工夫が必要になることがあります。
- JSON Lines形式 (
-
書き出し (
df.to_json): 大規模なDataFrameをto_jsonで書き出す際も、同様にメモリ使用量が増加する可能性があります。lines=Trueは行ごとに書き出すため、メモリ効率が良い方法です。それ以外のorientでは、DataFrame全体をメモリ上でJSON構造に変換してから書き出すため、メモリ消費が大きくなることがあります。大規模なDataFrameをファイルに書き出す場合は、orient='records'かつlines=Trueを検討するか、あるいはCSVやParquetといった別の形式での書き出しも選択肢に入れると良いでしょう。
大規模データを扱う際には、JSONデータの構造を理解し、メモリ使用量を抑えるための適切な読み込み・書き出し方法を選択することが重要です。ijson のような外部ライブラリとの連携も、選択肢の一つとして考慮に入れると良いでしょう。また、Daskのような大規模データ並列処理ライブラリが、Pandas風のAPIでJSONを扱える機能を提供している場合もあります。
実世界のデータ分析シナリオ
PandasとJSONの連携は、様々な実世界のデータ分析タスクで活用されています。
- Webスクレイピングデータの分析: WebサイトからスクレイピングしたデータがJSON形式で提供されることがあります。例えば、商品の詳細情報、レビュー、不動産情報などがJSON構造になっている場合があります。これらのJSONをPandasで読み込み、商品カテゴリごとの集計、レビューのセンチメント分析(別途自然言語処理ライブラリと組み合わせる)、地域ごとの価格比較などを行うことができます。ネストした情報(例: 各商品の複数のレビュー)は
json_normalizeで展開して分析することが多いでしょう。 - ログデータの分析: Webサーバーのアクセスログやアプリケーションのログは、可読性と機械処理の容易さからJSON形式(特にJSON Lines形式)で出力されることが増えています。これらのログファイルを
pd.read_json(lines=True)で読み込み、ユーザー行動の追跡、エラーログの特定と集計、パフォーマンスメトリクスの分析などを行います。特定のイベントパラメータがネストしている場合は、ここでもjson_normalizeが役立ちます。 - 設定ファイルの読み込みと処理: アプリケーションや分析スクリプトの設定ファイルがJSON形式になっていることがあります。
json.loads()でPython辞書として読み込むのが一般的ですが、設定項目が複雑で階層的な構造を持つ場合や、複数の設定ファイルを結合して分析したい場合などに、一度Pandas DataFrameとして読み込むことで、設定項目間の関係を分析したり、設定のバリデーションを行ったりすることが可能になります。 - 非リレーショナルデータベース(NoSQL)との連携: MongoDBのようなドキュメント指向データベースはデータをBSON(JSONに似たバイナリ形式)で格納します。これらのデータベースからデータをエクスポートしたり、直接Pythonドライバ(
pymongoなど)で取得したりすると、JSON形式(またはPythonの辞書/リスト)でデータが得られます。これをPandas DataFrameに変換して、リレーショナルデータベースでは難しい柔軟なクエリや分析を行うことができます。 - データレイク/クラウドストレージ上のJSONL/JSONデータの処理: Amazon S3, Google Cloud Storage, Azure Blob StorageなどのクラウドストレージにJSON Lines形式やJSON形式で格納されたデータを、Pandasの
read_jsonがサポートするURLパスや、s3fs,gcsfs,adlfsなどのファイルシステムライブラリと連携して直接読み込み、分析することができます。これはクラウドベースのデータ分析パイプラインで一般的なパターンです。
これらのシナリオからもわかるように、PandasとJSONの連携スキルは、現代の多様なデータソースを扱うデータ分析者にとって不可欠なものとなっています。
まとめと展望
本記事では、PandasとJSONの連携について、基本的な読み込み・書き出しから、ネストした構造の取り扱い、応用的な連携、エラー処理、パフォーマンス考慮まで、詳細に解説しました。
- JSONは軽量で人間が読めるデータ交換フォーマットであり、Web APIや設定ファイルなど様々な場所で利用されています。
- PandasはPythonでデータ分析を行うための強力なライブラリであり、DataFrameを使って表形式データを効率的に扱えます。
pd.read_json()関数は、様々な形式のJSONデータをDataFrameに読み込むための主要なツールであり、orientパラメータでJSON構造とDataFrameのマッピング方法を指定します。JSON Lines形式 (lines=True) の読み込みもサポートしています。df.to_json()メソッドは、DataFrameを様々な形式のJSONとして書き出すために使用され、こちらもorientパラメータが重要です。JSON Lines形式 (lines=True) での書き出しも可能です。- ネストしたJSONデータは、Pythonオブジェクトとして読み込んだ後、
pd.json_normalize()関数を使ってフラット化することができます。record_pathで展開するリスト、metaで親要素の情報を指定することで、分析可能な表形式データに変換できます。複雑な構造には、explode()や複数回のjson_normalize適用などの組み合わせが有効です。 - Web APIからのJSONレスポンスの取得とPandasでの処理は一般的な応用シナリオです。
requestsライブラリと組み合わせることで効率的に実現できます。 - JSONデータの構文エラーや、読み込み後の欠損値、データ型の不一致など、現実的なデータ品質問題への対応方法も重要です。適切なエラーハンドリングや、Pandasのデータクリーニング機能(
fillna,dropna,astype,to_numericなど)を活用します。 - 大規模データを扱う際は、メモリ使用量を考慮し、JSON Lines形式でのチャンク読み込みや、ストリーミングパースが可能なライブラリとの連携などを検討する必要があります。
PandasとJSONの連携は、データ分析ワークフローにおいて非常に強力な組み合わせです。本記事で紹介した機能を習得することで、Webから取得したデータ、APIレスポンス、ログデータなど、JSON形式の多様なデータを自信を持って扱えるようになるでしょう。
データ分析の領域は常に進化しており、JSONのような柔軟なデータ形式の重要性は今後も増していくと考えられます。Pandasを使ったJSONデータの処理は、データ分析、データエンジニアリング、さらには機械学習の前処理など、幅広い分野で役立つ基礎スキルです。
本記事が、皆様のPandasとJSONを使ったデータ分析の旅の一助となれば幸いです。さらに学習を進める際は、PandasやRequestsライブラリの公式ドキュメントを参照したり、実際に様々な形式のJSONデータを自分で生成・処理してみたりすることをお勧めします。
Happy Data Analyzing!