"気の利いた"擬似データを生成したい

# この記事は
[NSSOL advent carendar](https://qiita.com/advent-calendar/2021/nssol) 12/23担当分です。よろしくお願いします。

昨日は[研修を運営してみて思ったこと](https://note.com/kofsd_52329/n/nd702c3a5d76b)でした。
研修対応すると、その後も割と忙しくて、振り返りの時間がちゃんと取れなかったりします。
まとまった現場知見・感想が読めるのって、ありがたいなと思いました。

さて、今回のテーマは、「疑似データ生成」です。

## 背景：実データの取得は大変　擬似データが使えるかも
データ分析やシステム開発のために、実データかそれに近いデータが欲しくなることは多々あります。
ただ、顧客情報や営業秘密といった機微な情報が含まれる場合は、データ取得までに高いハードルがあることが多いです。
結果、試してみたいアイディア/製品/分析手法などの適用ができないこともあるかと思います。

![スクリーンショット 2021-12-22 20.06.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5897/8ecb5d9a-188e-a539-8e25-2c39a7c11398.png)

解決策の1つとして、擬似データの利用、が挙げられそうです。[参考](https://mostly.ai/use-case/rapid-data-poc-with-synthetic-data/)
実データを入力して、データの形式や統計量・分析結果など保存してほしい性質を残しつつ、実データとは一定以上異なる安全な擬似データ...を生成することで、

- 有用性の確保：システム開発・分析のPoCに必要な、実データに近いデータの確保 
- 安全性の確保：実データの提供による情報漏洩事故の回避

ができるかもしれません。[^1]
[^1]: 今回は[faker](https://github.com/faker-ruby)のような、ゼロから擬似データを作る、というアプローチは考えません。データを入力とする疑似データ生成を対象にします。

![スクリーンショット 2021-12-22 20.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5897/d5f9de9c-db98-d755-875e-7108fba15043.png)

## 擬似データ生成手法と課題

数多くの擬似データ手法が提案・製品化されています。[^2]
[^2]: ここで挙げた製品群や手法について、2021年現在当社が販売・導入等のご支援を行えるわけではありません　すみません がんばります

### 擬似データ生成を行える製品・ソフトウェア (調査中) 
- システムエグゼの [テストエース](https://www.system-exe.co.jp/product/db/testace/)
    - システム開発用のテストデータ生成用のツールです
    - ダミー氏名や、ダミー郵便番号とそれに対応したダミー住所、までやってくれるのは便利そうですね
    - 「数値のランダム変換」などを行うと、統計的な性質は失われそうです。分析用途なら、他の仕組みが適していそうです
- [Mostly AI](https://mostly.ai/synthetic-data-platform/generate-synthetic-data/)の Synthetic Data Platform
    - 擬似データ生成のツールです。元データからは一定以上異なり、かつ統計的な性質を上手く保持したデータを生成してくれます
    - データ分析・活用のPoC用のデータ生成、に利用できそうです。
    - 擬似データ生成コンテスト[PWSCUP2020](https://www.iwsec.org/pws/2020/cup20.html)でも好成績でした。
- NTTの[Tasokarena](https://www.ntt-tx.co.jp/products/anontool/)
    - 匿名加工を行うツールです。
    - 最近、擬似データ生成の手法が実装されたみたいです。[参考](https://www.ntt-tx.co.jp/whatsnew/2021/211026.html)
- etc..

### 擬似データ生成手法 (調査中)
GANベースの手法か、確率モデルベースの手法か、で一旦分類してみています。

- GANベースの手法 (**GAN)
    - MedGAN [[arXiv:1703.06490](https://arxiv.org/abs/1703.06490)][[GitHub](https://github.com/mp2893/medgan)]
    - TableGAN [[arXiv:1806.03384](https://arxiv.org/abs/1806.03384)][[GitHub](https://github.com/mahmoodm2/tableGAN)]
    - TGAN [[arXiv:1811.11264](https://arxiv.org/abs/1811.11264)][[GitHub](https://github.com/sdv-dev/TGAN)]
        - [触ってみた記事](https://upura.hatenablog.com/entry/2019/08/20/120000)もありました
        - 分析用データへの擬似データの追加、を試されています
    - CTGAN [[arXiv:1907.00503](https://arxiv.org/abs/1907.00503)][[GitHub](https://github.com/sdv-dev/CTGAN)]
        - [触ってみた記事](https://qiita.com/jovyan/items/c41ab61a6b04e9a6e4df)もありました。
        - 擬似データの追加により、XGBoostによる分析にどのような影響が出るか、を検証されています
- 確率モデルベースの手法
    - [コピュラベースの手法](https://arxiv.org/abs/2101.00598)
    - [統計モデリングベースの手法](https://sdv.dev/SDV/user_guides/relational/hma1.html#what-is-hma1)
- その他
    - [統計値を用いたプライバシ保護擬似データ生成手法](https://ipsj.ixsq.nii.ac.jp/ej/index.php?active_action=repository_view_main_item_detail&page_id=13&block_id=8&item_id=187369&item_no=1)
        - 上述のNTTの匿名加工ツールにも実装されていそうです
- etc..

### 擬似データ生成の課題
擬似データの品質が気になることがあります。

例えば、「年齢」「職業」という項目を含むデータを擬似生成した時、「1歳、会社役員」というデータは、あまり含んでほしくありません。
（不安に思って調べてみたら、[会社役員は10歳からOK](http://www.seturitu-hiroba.jp/14692273714719)らしいです。知りませんでした...）

![yakuin.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5897/5fc0b82a-4c46-5285-249e-4990177e6822.png)

他にも、ID列に重複があったり、給料がマイナスの値があったりしても困ります。業務ルールや常識を反映したい場面がありそうですが、列ごとに確率分布からサンプリングしたりすると、こういうあり得ないデータが頻繁に生成されてしまいます。

擬似データにも現実味を持たせるために、ある程度の「制約」を反映する方法を探したくなりました。

## 今回やること：SDVを使って、制約を反映した擬似データを生成する
常識や業務的なルールを反映...しやすそうな仕組みを持つ擬似データ生成の仕組みとして、[「SDV」](https://github.com/sdv-dev/SDV)というライブラリを見つけました。

少しだけ触ってみます。
 
[チュートリアル](https://sdv.dev/SDV/user_guides/single_table/handling_constraints.html)を参考に、実際に動かしてみます。ノートブックをColabで用意してみましたので、もしご興味ありましたら動かしてみてください。

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/gist/gghatano/2db38e4680d5aa0326d49c68e7f17026/-sdv.ipynb)

# 実験：SDVによる制約付き擬似データ生成

## SDVのインストール
pipから導入できます。便利ですね。

```python
! pip install sdv
from sdv.demo import load_tabular_demo
import pandas as pd
```
デモデータを参照します。

```python
employees = load_tabular_demo()
print(employees.columns)
employees.head()
```
| |company|department|employee_id|age|age_when_joined|years_in_the_company|salary|annual_bonus|prior_years_experience|full_time|part_time|contractor|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|0|Pear|Sales|1|39|36|3|83500.00|21000.00|5|1.0|0.0|0.0|
|1|Pear|Design|5|36|29|7|103581.36|23922.77|4|0.0|0.0|1.0|
|2|Glasses|AI|1|37|34|3|112000.00|19500.00|2|1.0|0.0|0.0|
|3|Glasses|Search Engine|7|35|26|9|82160.76|24167.84|2|0.0|0.0|1.0|
|4|Cheerper|BigData|6|40|38|2|159500.00|14500.00|5|0.0|1.0|0.0|


複数社の従業員の給料・雇用形態データです。こちらは当然疑似データです。

## データのルールの確認
制約を反映した擬似データを生成してみます。

よく観察してデータの意味を考えると、いくつかルールがありそう。例えば...

- 一意性の制約
  - $\text{company}$(会社)ごとに一意な$\text{employee_id}$(社員ID)が振られるはずなので、「($\text{company, employee_id}$)は一意」
- 不等式で表現できる制約
  - $\text{salary}$の値を考えると適当な範囲に収まるので、「$0 \leq \text{salary} \leq 2,000,000$」
  - $\text{age}$(今の年齢)と$\text{age_when_joined}$(入社した時の年齢)の関係を考えると、「$\text{age} \geq \text{age_when_joined}$」
  - $\text{age}$は年齢なので、従業員っぽい年齢(一旦16歳から80歳までとします)なはずなので、「$16\leq\text{age}\leq 80$」
- 等式で表現できる制約
  - 値の関係を考えると、「$\text{age} - \text{age_when-joined} = \text{years_in_the_company}$」
- そのほかいろいろ (分類するのが面倒になりました)
  - $\text{full_time}$, $\text{part_time}$, $\text{contractor}$は、どれかが1で他が0になっている(3種類の値が含まれる契約形態列をone-hot-encodingしたもの、という背景があるみたいです)

などがあります。 [^3] 
[^3]: 擬似データの用途次第では、無視しても問題ない制約もあると思います。

SDVにはさまざまな種類の制約を反映した擬似データを作る...ための仕組みが用意されています。それぞれ実装して疑似データに反映してみます。 [^4]

[^4]: 前処理や後処理で対応する方が良さそうなもの...もあると思います。ただ、管理の手間の観点で、データのルールを全部擬似データ生成のところに反映しておく、という戦略もありそうです。処理内容が散在していると管理がめんどくさそうなので。

## データ制約の実装・反映
実装例を一部載せておきます。実装の詳細はColabに書くことにします。

### 一意性制約の反映
[sdv.constraints.Unique](https://sdv.dev/SDV/api_reference/constraints/api/sdv.constraints.UniqueCombinations.html)を使います。
一意になる列の組を入力します。今回だと、会社名と従業員idです。

```python
from sdv.constraints import Unique
unique_employee_id_company_constraint = Unique(
    columns = ['company', 'employee_id'],
)
```

他にも、いろいろ制約を実装してみました。
記事が長くなってしまうので、詳細が気になる方はColabを見てください。
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/gist/gghatano/2db38e4680d5aa0326d49c68e7f17026/-sdv.ipynb)

### 制約の反映・データ生成

データ生成の手法は、今回は[CTGAN](https://github.com/sdv-dev/CTGAN)を使います。SDVに実装されている他のデータ生成手法(TGAN、Copula、etc...)でも、同じように使えます。

試しに、一部制約を反映しない版も準備しておいて、生成されたデータを比較してみます

```python
## 制約の反映
from sdv.tabular import CTGAN

## 実装してみた制約一覧
constraints = [
               unique_employee_id_company_constraint, ## 会社ごとにIDが一意ですよ制約
               age_gt_age_when_joined_constraint, ## ageはage_when_joinedより大きいぞ制約
               reasonable_salary_constraint, ## salaryは適当な範囲(0-2,000,000)だぞ制約
               years_in_the_company_constraint, ## 在籍年数は、年齢-入社年齢だぞ制約
               reasonable_age_constraint, ## 年齢は、適当な範囲(16-80)だぞ制約
               one_hot_constraint, ## 契約形態の3列はone-hot-encodingで作ったよ制約
]

## 一部の制約を外す：在籍年数と、one-hot-encodingの制約を外してみた版
constraints_without_formula = [
               unique_employee_id_company_constraint, ## 会社ごとにIDが一意ですよ制約
               age_gt_age_when_joined_constraint, ## ageはage_when_joinedより大きいぞ制約
               reasonable_salary_constraint, ## salaryは適当な範囲(0-2,000,000)だぞ制約
               # years_in_the_company_constraint, ## 在籍年数は、年齢-入社年齢だぞ制約
               reasonable_age_constraint, ## 年齢は、適当な範囲(16-80)だぞ制約
               # one_hot_constraint, ## 契約形態の3列はone-hot-encodingで作ったよ制約
]


## 学習・データ生成
## 制約なし版
ct = CTGAN()

## 制約を全て反映した版
ct_constraint = CTGAN(constraints=constraints)

## 制約を一部反映していない版
ct_constraint_ = CTGAN(constraints=constraints_without_formula)

## 学習
ct.fit(employees)
ct_constraint.fit(employees)
ct_constraint_.fit(employees)

## データ生成
sampled_ct = ct.sample(100)
sampled_ct_constraint = ct_constraint.sample(100)
sampled_ct_constraint_ = ct_constraint_.sample(100)
```
さて、データを確認してみましょう。

## データの確認
### 制約なし版
```python
## 内容確認
sampled_ct.head()
```

| |company|department|employee_id|age|age_when_joined|years_in_the_company|salary|annual_bonus|prior_years_experience|full_time|part_time|contractor|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|0|Glasses|Support|80|33|33|1|144000.00|19900.27|2|0.0|0.0|1.0|
|1|Cheerper|BigData|79|45|38|5|144000.00|10640.13|5|0.0|0.0|0.0|
|2|Pear|Sales|1|37|30|9|138608.64|19900.27|1|0.0|1.0|0.0|
|3|Pear|Support|1|40|35|8|58648.49|19900.27|5|1.0|0.0|1.0|
|4|Cheerper|Design|68|33|31|2|86882.62|9930.32|5|0.0|0.0|1.0|

2,3行目で、(会社、社員ID)の(Pear, 1)が重複していて、一意になっていません。困ります。

では、ちゃんと制約を反映した版を見てみましょう。

### 制約を反映した版
```python
## 内容確認
sampled_ct_constraint.head()
```

| |company|department|employee_id|age|age_when_joined|years_in_the_company|salary|annual_bonus|prior_years_experience|full_time|part_time|contractor|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|0|Pear|Support|1|44|30|14|50328.417643|18698.33|2|0.0|1.0|0.0|
|1|Pear|AI|3|39|30|9|27977.662781|12458.97|2|1.0|0.0|0.0|
|2|Pear|Search Engine|43|49|39|10|29318.168727|19900.27|4|1.0|0.0|0.0|
|3|Glasses|Design|21|45|30|15|27384.865234|19900.27|5|0.0|1.0|0.0|
|4|Cheerper|Sales|1|46|30|16|33498.534931|19900.27|4|0.0|1.0|0.0|

おお、できていそうです。
IDの制約と、age,age_when_joined,years_in_the_companyの関係と、one-hot-encodingもちゃんと反映されていそう。

では、一部だけ制約を外した版を見てみます。

### 制約を一部反映していない版
```python
## 内容確認
sampled_ct_constraint_.head()
```

| |company|department|employee_id|age|age_when_joined|years_in_the_company|salary|annual_bonus|prior_years_experience|full_time|part_time|contractor|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|0|Pear|BigData|1|50|34|6|66627.734809|15059.80|4|1.0|0.0|1.0|
|1|Cheerper|Design|1|41|30|5|25734.434223|19900.27|3|1.0|1.0|1.0|
|2|Cheerper|Search Engine|8|38|35|2|71856.151940|19900.27|2|0.0|0.0|0.0|
|3|Glasses|Support|14|40|34|1|48165.690454|19900.27|3|0.0|1.0|1.0|
|4|Glasses|Sales|1|31|30|6|104812.032219|12291.97|3|0.0|1.0|0.0|

ふむ...years_in_the_companyがルールにあっていません。

右端3列のone-hot-encodingルールも満たしていませんね。

一旦おしまいです。

## 感想
制約を実装して反映する...ところまでは、できました。

実務的には、都度実装するのはちょっと大変です。
DBMSでテーブルを定義した際、業務ルールを今回のように制約として実装しておいて、データ生成が必要な時には呼び出すだけ...という運用はあり得るかも。
[リレーションを考慮したデータ生成](https://sdv.dev/SDV/user_guides/relational/index.html)もできたりしますし。

データベース側の外部キー制約やcheck制約を読み取って、SDVでの制約の記述を自動生成できたりすると、楽に擬似データが作れそうです。

## 次にやること
今後技術的に調査したいこととしては...

- 実データ vs 制約反映済み擬似データで、
    - 記述統計の比較
    - 目的を絞ったデータ分析・機械学習の結果の比較
    - 安全性の検証
- 効率的にデータ生成を行えるような制約の書き方
    - 制約が増えてくると、学習・生成に時間を要しそうです
    - reject-sampling：ルールに合わないデータを除外する手法を使いすぎると、サンプリングの回数がネックになる？
- データ生成の制約と、差分プライバシー等安全性基準の組み合わせ

があります。

差分プライバシー基準での安全性と、業務ルールの反映を両立する...という研究は、既にあるかな？調べてみます。

以上です。

## 明日は

@kmnky さんの投稿です！楽しみですね！