はじめに
データエンジニアとして仕事をしていると、同じ「DBからデータを取得して加工する」処理でも、用途によって最適な技術や手法が変わることがあります。
今回は、私が実務で経験した「サイト表示用のJSON生成」と「分析用データ処理」の違いをPythonの標準ライブラリとpandasの比較を通して整理してみました。
今回のサイト表示用JSONの特徴
今回のケースは、ECサイト向けの商品マスタJSONを生成する処理です。ポイントは以下です。
- DBからマスタを取得し、値の補完やURL加工を行う
- 集計や結合など複雑な計算は不要
- フロントエンドで読みやすいJSONを出力する
- データ件数は数万〜数十万件程度
このような用途では、軽量かつシンプルな処理が求められます。
分析用データとの違い
分析用データは、集計や結合、フィルタリング、統計計算が中心です。DataFrameで管理すると次のような利点があります。
- SQLだけでは複雑になる集計処理を簡単に書ける
- 欠損値処理や型変換、結合などを効率的に行える
- 数百万件規模のデータでも比較的簡単に扱える
一方、単純なJSON出力だけならpandasは不要で、処理時間やメモリ効率の面で標準ライブラリの方が有利です。
標準ライブラリ vs pandasの処理時間比較
作ったコード
GitHubにもあります。詳しい検証方法等はそちらで。
crteate_db.py
import sqlite3
import argparse
from faker import Faker
import random
def init_db(n=10, db_path="sample.db"):
fake = Faker()
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS products")
cur.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL,
discount REAL,
image_path TEXT
)
""")
for i in range(1, n + 1):
name = fake.word().title()
# 値段をランダムに欠損させる
price = None if i % 5 == 0 else round(random.uniform(500, 5000), 2)
# 割引をランダムに入れる
discount = round(random.uniform(0, 0.3), 2) if price is None else None
image_path = f"/images/{name.lower()}.png"
cur.execute(
"INSERT INTO products (id, name, price, discount, image_path) VALUES (?, ?, ?, ?, ?)",
(i, name, price, discount, image_path)
)
conn.commit()
conn.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--n", type=int, default=10, help="Number of records to insert")
args = parser.parse_args()
init_db(n=args.n)
print(f"Database created with {args.n} records.")
use_json.py
import sqlite3
import json
import os
DB_PATH = "sample.db"
CURRENT_JSON = "current.json"
OUTPUT_JSON = "new.json"
def fetch_from_db(db_path=DB_PATH):
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("SELECT id, name, price, discount, image_path FROM products")
rows = cur.fetchall()
conn.close()
return rows
def transform_record(row):
id_, name, price, discount, image_path = row
# 値段補完
if price is None and discount is not None:
price = round(1000 * (1 - discount), 2)
# 画像URL変換
image_url = f"https://example.com{image_path}"
return {
"id": id_,
"name": name,
"price": price,
"image_url": image_url
}
def load_current(path=CURRENT_JSON):
if not os.path.exists(path):
return []
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def detect_diff(current, new_records):
"""新規 or 値が変わったレコードを抽出"""
current_map = {item["id"]: item for item in current}
diff = []
for rec in new_records:
old = current_map.get(rec["id"])
if old is None:
# 新規
diff.append(rec)
else:
# 値の差分チェック
if json.dumps(old, sort_keys=True) != json.dumps(rec, sort_keys=True):
diff.append(rec)
return diff
def save_json(data, path=OUTPUT_JSON):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def main():
db_rows = fetch_from_db()
transformed = [transform_record(r) for r in db_rows]
current = load_current()
diff = detect_diff(current, transformed)
if diff:
print(f"{len(diff)} records with changes found. Writing to {OUTPUT_JSON}")
save_json(diff)
else:
print("No differences found.")
# Lambda互換
def lambda_handler(event=None, context=None):
main()
return {"status": "done"}
if __name__ == "__main__":
main()
use_pandas.py
import sqlite3
import pandas as pd
import os
DB_PATH = "sample.db"
CURRENT_JSON = "current.json"
OUTPUT_JSON = "new.json"
def fetch_from_db(db_path=DB_PATH):
conn = sqlite3.connect(db_path)
df = pd.read_sql_query(
"SELECT id, name, price, discount, image_path FROM products", conn
)
conn.close()
return df
def transform(df: pd.DataFrame) -> pd.DataFrame:
# 値段補完
df["price"] = df.apply(
lambda r: round(1000 * (1 - r["discount"]), 2) if pd.isna(r["price"]) and pd.notna(r["discount"]) else r["price"],
axis=1
)
# 画像URL変換
df["image_url"] = "https://example.com" + df["image_path"].astype(str)
# 出力カラムだけに絞る
return df[["id", "name", "price", "image_url"]]
def load_current(path=CURRENT_JSON) -> pd.DataFrame:
if not os.path.exists(path):
return pd.DataFrame(columns=["id", "name", "price", "image_url"])
return pd.read_json(path, orient="records")
def save_json(df: pd.DataFrame, path=OUTPUT_JSON):
df.to_json(path, orient="records", indent=2, force_ascii=False)
def main():
df = fetch_from_db()
transformed = transform(df)
save_json(transformed)
current = load_current()
if current.empty:
print(f"{len(transformed)} records found. Writing to {OUTPUT_JSON}")
else:
# 差分チェック
merged = transformed.merge(current, on="id", how="left", suffixes=("", "_old"))
changed_count = 0
for _, row in merged.iterrows():
if pd.isna(row.get("name_old")):
changed_count += 1
else:
old = {c: row[f"{c}_old"] for c in ["name", "price", "image_url"]}
new = {c: row[c] for c in ["name", "price", "image_url"]}
if old != new:
changed_count += 1
if changed_count:
print(f"{changed_count} records with changes found. Writing to {OUTPUT_JSON}")
else:
print("No differences found.")
def lambda_handler(event=None, context=None):
main()
return {"status": "done"}
if __name__ == "__main__":
main()
main.py
import time
import use_json
import use_pandas
def main():
# 標準ライブラリ版
start = time.time()
use_json.main()
end = time.time()
print(f"標準ライブラリ版処理時間: {end - start:.4f} 秒")
# pandas版
start = time.time()
use_pandas.main()
end = time.time()
print(f"pandas版処理時間: {end - start:.4f} 秒")
if __name__ == "__main__":
main()
実行結果
これらのコードを使い、私の環境で200,000件のデータを生成した場合、以下の結果になりました。

- JSON生成だけのケースでは、pandasは内部オブジェクト管理などのオーバーヘッドがあるため標準ライブラリより遅くなる
- 集計や結合などが入る場合はpandasの方が圧倒的に便利
まとめ
| 観点 | 標準ライブラリ版 (use_json.py) |
pandas版 (use_pandas.py) |
|---|---|---|
| 想定シーン | Lambdaなど軽量環境 | データ基盤や分析処理 |
| 実装スタイル | forループ・辞書操作 | DataFrame演算 |
| 処理速度 | 小規模データで有利 | 大規模データで有利 |
| メモリ効率 | 少ない | 多め(全件展開) |
| 保守性 | シンプル | 分析者に馴染みやすい |
- サイト表示用JSON生成のような単純なレコード整形・出力 には標準ライブラリで十分
- 分析用データの集計・結合・統計計算にはpandasが適している
- データエンジニアとしては、用途に応じた技術選択が重要であることを意識することが大切