1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サイト表示用JSON生成と分析用データ処理の違いについて考えてみた

Posted at

はじめに

データエンジニアとして仕事をしていると、同じ「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件のデータを生成した場合、以下の結果になりました。
image.png

  • JSON生成だけのケースでは、pandasは内部オブジェクト管理などのオーバーヘッドがあるため標準ライブラリより遅くなる
  • 集計や結合などが入る場合はpandasの方が圧倒的に便利

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?