💽

リアルなダミーデータを自在に生成!PythonライブラリSDVで「値のバリエーション」を拡張するテクニックと活用法📈

に公開

はじめに

機械学習のモデル開発やBIツールのテスト、データ分析の学習教材を作成する際、大量のリアルなダミーデータが必要になることはありませんか?

実際のプロジェクトでは、以下のような課題に直面することがよくあります:

  • データ分析の学習用教材:受講者に実践的な演習をしてもらうため、リアルな大規模データセットが必要
  • プロトタイプ開発:実データの一部しか入手できていないが、デモンストレーション用に大規模データが必要
  • システムテスト:本番環境に近い規模のデータでパフォーマンステストを実施したい
  • 機密性の問題:実データは使えないが、統計的特性を保持した代替データが必要

今回は、Pythonの合成データ生成ライブラリ「SDV(Synthetic Data Vault)」を使って、これらの課題を解決する方法を紹介します。

使用するダミーデータについて

本記事では、Tableauが提供する「Sample - Superstore」データセットを使用します。このデータセットは:

  • 日本語版の高品質な擬似データとして貴重な存在
  • BIツールの学習やデモで広く利用されている
  • 小売業の販売データ(注文、顧客、製品、売上など)を含む
  • 課題
    • 元データは約10,000件と少なめ
    • データの時期が2016年で最新ではない
    • 顧客数や製品数のバリエーションが限定的

今回は、この100件のサンプルを50,000件に拡張し、2016年から2025年までの時系列データに変換します。

完全なコード

記事で紹介したコードの完全版はこちらのGoogleColaboratoryでご覧いただけます。

本記事で生成したデータは以下のスプレッドシートに格納しています。
https://docs.google.com/spreadsheets/d/1JRS2YkkCc8n3NqHNU19JFUDN_0HEG-XbeVD41Rjzt5g/edit?gid=930438742#gid=930438742

Tableauスーパーストアデータの格納ドライブ が拡張前の大元のダミーデータです。(10,000件ありますが、本記事では100行⇨数万件への拡張を試みています)

SDV(Synthetic Data Vault)とは

SDV(Synthetic Data Vault)は、表形式の合成データを生成するためのPythonライブラリです。SDVは、実データからパターンを学習し、それを合成データで再現するために、さまざまな機械学習アルゴリズムを使用します。

https://github.com/sdv-dev/SDV

SDVの主な特徴

  1. 多様なモデルのサポート

    • 古典的な統計手法(GaussianCopula)から深層学習手法(CTGAN)まで、複数のモデルを提供
    • 単一テーブル、複数の関連テーブル、時系列データに対応
  2. 評価と可視化機能

    • 合成データと実データを様々な指標で比較
    • 品質レポートの生成により詳細な洞察を取得
  3. 前処理と制約の定義

    • データ処理の制御、匿名化の選択、ビジネスルールの論理的制約の定義が可能

SDVの起源と開発

SDVプロジェクトは2016年にMITのData to AI Labで最初に作成されました。4年間の研究と企業での実績を経て、2020年にプロジェクトの成長を目的としてDataCeboが設立されました。

SDVは既存のデータの統計的傾向を学習して、各種制約も必要に応じて与えながら、"それっぽい"データを生成できるツールです。

本記事のアプローチ:SDVと自作ロジックの「良いとこ取り」でリアルなデータを作る

SDVは非常に便利ですが、まるで「優秀なモノマネ芸人」のような特性を持っています。元ネタ(元データ)の特徴を捉えるのは得意ですが、全く新しいネタ(新しい値)をゼロから生み出すのは苦手です。

😵 SDVだけでは難しいこと(課題の具体例)

実際に使ってみると、以下のような壁にぶつかります。

  • 新しい顧客や製品を作れない
    元データに「山田さん」と「佐藤さん」しかいなければ、生成されるデータも「山田さん」と「佐藤さん」だけです。全く新しい「鈴木さん」は登場しません。製品名も同様で、これではデータのバリエーションが不足します。

  • 未来のデータは作れない
    2016年のデータだけを学習させても、SDVが自動で2025年までの未来のデータを生成してくれるわけではありません。時間の流れを拡張するには、一工夫が必要です。

💡 解決策:役割分担でダミーデータを拡張する

そこでこの記事では、**SDVと自作のPythonコードを組み合わせる「ハイブリッド戦略」**を取ります。それぞれの得意なことを活かして、役割分担をさせます。

  1. SDVの役割 → データの「骨格」作り
    売上と利益の関係、注文の頻度、カテゴリごとの売上比率といった、**データ全体の「統計的な雰囲気」**を学習させ、データの量を増やす役割を任せます。

  2. 自作コードの役割 → データの「肉付け」
    SDVが苦手な**「新しい値の創造」**を担当します。

    • 顧客名・製品名:一般的な姓名リストや命名規則を元に、リアルな名前を大量に生成し、バリエーションを豊かにします。
    • 日付:生成されたデータの日付を、2016年から2025年まで広がるようにプログラムで強制的に調整します。

📝 この記事で実現する処理の流れ

この「良いとこ取り」のアプローチは、**「土台を拡張し、後からバリエーションを被せる」**という流れです。

つまり、こういうことです!

①SDVで「値」の部分を拡張:売上や利益といった数値データの関係性を保ったまま、データ行を5万件に増やす。
②自作コードで「バリエーション」を用意:新しい顧客名(8,000人分)や製品名(100種)のリストを作る。
③最後にマッピング:1で作ったデータに対し、2のリストからランダムに選んだ名前や、調整した日付を「上書き」していく。(JOINに近いイメージ)

このステップを踏むことで、単なるデータの水増しではなく、質・量・バリエーションの全てを兼ね備えた、非常にリアルで実践的な大規模データセットを生成することができるのです。

環境準備

まずは必要なライブラリをインストールします。

pip install sdv pandas numpy

元データの構造

今回使用するのは、以下のような構造を持つ販売データ(superstore.csv)です:

カラム名 データ型 説明
行 ID Integer 連番ID
オーダー ID String 注文ID(JP-2021-0000001形式)
オーダー日 String 注文日(YYYY/MM/DD)
出荷日 String 出荷日(YYYY/MM/DD)
顧客名 String 顧客の氏名
顧客 ID String 顧客ID(CG-12520形式)
製品名 String 商品名
製品 ID String 製品ID(FUR-BO-10001798形式)
カテゴリ String 商品カテゴリ(家具、事務用品、機器)
売上 Float 売上金額
数量 Integer 購入数量
利益 Float 利益額

元データは2016年の100件のみで、顧客数も製品数も限られています。

実装

1. 基本的なSDVによるデータ生成

まず、SDVの基本的な使い方から始めます:

import pandas as pd
from sdv.metadata import SingleTableMetadata
from sdv.single_table import GaussianCopulaSynthesizer

# データの読み込み
df = pd.read_csv('superstore.csv')

# メタデータの作成
metadata = SingleTableMetadata()
metadata.detect_from_dataframe(df)

# データ型の設定
metadata.update_column(column_name='行 ID', sdtype='id')
metadata.update_column(column_name='オーダー日', sdtype='datetime')
metadata.update_column(column_name='顧客名', sdtype='categorical')
metadata.update_column(column_name='売上', sdtype='numerical')

# シンセサイザーの作成とトレーニング
synthesizer = GaussianCopulaSynthesizer(
    metadata,
    enforce_min_max_values=True
)
synthesizer.fit(df)

# 合成データの生成
synthetic_data = synthesizer.sample(num_rows=5000)

2. 時系列データへの拡張

単純に行数を増やすだけでなく、2016年から2025年までの時系列データに拡張します:

from datetime import datetime, timedelta
import numpy as np

# 年ごとにデータを生成
years_to_generate = range(2016, 2026)  # 2016年から2025年まで
rows_per_year = 5000  # 各年5000行(合計50000行)

all_synthetic_data = []

for year in years_to_generate:
    print(f"{year}年のデータを生成中...")
    
    # 各年のデータを生成
    yearly_data = synthesizer.sample(num_rows=rows_per_year)
    
    # 日付を各年に調整
    start_date = pd.Timestamp(f'{year}-01-01')
    end_date = pd.Timestamp(f'{year}-12-31')
    
    # ランダムな日付を生成
    random_days = np.random.randint(0, (end_date - start_date).days + 1, size=rows_per_year)
    yearly_data['オーダー日'] = [start_date + timedelta(days=int(d)) for d in random_days]
    
    # 出荷日をオーダー日から1-7日後に設定
    shipping_delays = np.random.randint(1, 8, size=rows_per_year)
    yearly_data['出荷日'] = [order_date + timedelta(days=int(delay)) 
                           for order_date, delay in zip(yearly_data['オーダー日'], shipping_delays)]
    
    all_synthetic_data.append(yearly_data)

# 全データを結合
synthetic_data = pd.concat(all_synthetic_data, ignore_index=True)

3. カーディナリティの制御(ここがポイント!)

SDVは既存データの統計的特性を学習しますが、新しい顧客名や製品名を創造的に生成することは苦手です。そこで、独自のロジックを組み合わせます。

顧客名の生成(8,000名まで拡張)

import random

# 日本人の姓名データを準備(実際は100種類の姓×60種類の名)
japanese_surnames = ['山田', '佐藤', '鈴木', '高橋', '田中', '伊藤', '渡辺', '中村', '小林', '加藤',
                    # ... 100種類の姓
                    ]
japanese_names = ['太郎', '花子', '一郎', '美咲', '健太', '愛', '翔太', '結衣', '大輔', 'さくら',
                    # ... 60種類の名
                    ]

def generate_new_customer_names(count, existing_names):
    """重複を避けながら新しい顧客名を生成"""
    new_names = set()
    
    while len(new_names) < count:
        surname = random.choice(japanese_surnames)
        name = random.choice(japanese_names)
        full_name = f"{surname} {name}"
        
        if full_name not in existing_names and full_name not in new_names:
            new_names.add(full_name)
    
    return list(new_names)

# 8,000名まで拡張
target_customer_count = 8000
existing_customers = df['顧客名'].unique().tolist()
new_customer_count = target_customer_count - len(existing_customers)
new_customers = generate_new_customer_names(new_customer_count, set(existing_customers))
all_customers = existing_customers + new_customers

製品名の生成(100個に制限)

# カテゴリごとの製品プレフィックス・サフィックス
product_prefixes = {
    '機器': ['プロ', 'スーパー', 'ハイパー', 'メガ', 'ウルトラ'],
    '事務用品': ['エコ', 'スマート', 'コンパクト', 'デラックス'],
    '家具': ['モダン', 'クラシック', 'エレガント', 'シンプル']
}

product_suffixes = {
    '機器': ['2000', '3000', 'X', 'Z', 'プラス'],
    '事務用品': ['セット', 'パック', 'ボックス', 'キット'],
    '家具': ['タイプA', 'タイプB', 'モデル', 'スタイル']
}

def generate_new_product_names(base_products, category, count):
    """カテゴリに応じた新製品名を生成"""
    new_products = []
    for _ in range(count):
        base = random.choice(base_products)
        if category in product_prefixes:
            prefix = random.choice(product_prefixes[category])
            suffix = random.choice(product_suffixes[category])
            new_name = f"{prefix} {base} {suffix}"
        else:
            new_name = base
        new_products.append(new_name)
    return new_products

4. 生成データへの適用

SDVで生成したデータに、拡張した顧客名と製品名を適用します:

# データ生成時に顧客名と製品名を置き換え
for idx, row in yearly_data.iterrows():
    # 顧客名をランダムに選択(8,000名から)
    yearly_data.loc[idx, '顧客名'] = random.choice(all_customers)
    
    # カテゴリに応じた製品名を選択(100個から)
    category = row['カテゴリ']
    if category in all_products_by_category:
        yearly_data.loc[idx, '製品名'] = random.choice(all_products_by_category[category])

実行結果

最終的に以下のようなデータセットが生成されます:

✅ 2016年から2025年までの50000行の合成データを保存しました。

生成されたデータの概要:
- 行数: 50000
- 列数: 20
- 期間: 2016年1月 から 2025年12月
- 顧客数: 8000
- 製品数: 100
- 平均注文数/顧客: 6.3
- 平均注文数/製品: 500.0

=== カーディナリティの比較 ===
顧客名: 134 → 8000
製品名: 73 → 100

ポイントと注意点

1. SDVのバージョンによるAPI変更

SDVは活発に開発されており、バージョンによってAPIが変わることがあります。例えば:

# 古いバージョン(エラーになる)
metadata.add_constraint(...)

# 新しいバージョン
from sdv.constraints import Inequality
constraint = Inequality(low_column_name='オーダー日', high_column_name='出荷日')
synthesizer.add_constraints([constraint])

2. 日本語データの取り扱い

日本語を含むCSVファイルを扱う場合は、エンコーディングに注意:

# 読み込み時
df = pd.read_csv('superstore.csv', encoding='utf-8')

# 保存時(BOMを付けてExcelでも文字化けしないように)
synthetic_data.to_csv(output_filename, index=False, encoding='utf-8-sig')

3. データの品質確認

生成したデータは必ず品質を確認しましょう:

# 欠損値の確認
print(synthetic_data.isnull().sum())

# 統計情報の確認
print(synthetic_data[numerical_columns].describe())

# カテゴリごとの分布
print(synthetic_data['カテゴリ'].value_counts())

# 年ごとの売上統計
yearly_sales = synthetic_data.groupby('年')['売上'].agg(['sum', 'mean', 'count'])
print(yearly_sales)

まとめ

この手法を使えば、以下のような用途で活用できます:

  • 教育・トレーニング用データセット:受講者が実践的な分析を学べる大規模データ
  • デモンストレーション用データ:実データに近い特性を持つ安全なデータ
  • システムテスト用データ:本番環境に近い規模でのパフォーマンステスト
  • プライバシー保護:実データの統計的特性を保持しつつ、個人情報を含まないデータ

実務上では、追加で生成データの中身をもう少し検証して、SDVに必要となる制約条件を与える なども必要だと思われるのですが、基本的な拡張は今回の内容でも一定役割を果たせているかと思います。

皆様のプロジェクトのお役に立てれば幸いです。

参考リンク


この記事が役に立ったら、ぜひいいねをお願いします!質問やご意見もお待ちしています。

Discussion