Clean Roomsを6ヶ月本番運用してわかったこと|プライバシー保護とデータ共有の現実
広告主とメディア企業のデータ統合にClean Roomsを導入。本当にセキュアなデータ共有ができるのか、実装してみた結果とコンプライアンス対応の実態を実体験から解説します。
Clean Rooms データ共有2026|プライバシー保護とビジネス価値の両立を実装した
先日プロジェクトで、広告主とメディア企業のデータ統合にClean Roomsを導入したんです。最初は「本当にセキュアなデータ共有ができるのか」懐疑的だったんですが、実装してみたら予想以上に実用的で、6ヶ月の本番運用を通じて見えたことをシェアしたいです。
正直、データ共有って昔ながらのやり方だと、CSVやクエリ結果をメール送信するとか、FTPサーバーに置くとか、本当にセキュリティが心配なアプローチばかりでした。うちのチームも同じ悩みを抱えていて、コンプライアンスとビジネス要件のバランスに常に悩まされていたんですよね。
Clean Roomsが解決した、データ共有の3つの課題
実装前、うちのチームが直面していた問題は3つありました。
課題1:プライバイシー侵害のリスク
データを外部に共有する時点で、個人情報や機密情報が漏洩する可能性がありました。特に広告業界では、ユーザーのセグメント情報や購買行動データを扱うので、GDPRやCCPAの対応が必須です。
課題2:データ品質の把握が困難
共有前にデータを抽出・クレンジングする手作業が多く、その過程でデータが変形して、もらった側で「これ正しい?」って疑問が残ることばっかりでした。月単位でのやり取りが続いていて、時間がもったいない。
課題3:監査・コンプライアンスの負担
誰が何をアクセスしたか、どのデータが共有されたかを手作業で記録していて、SOC2審査の時に本当に大変でした。
Clean Roomsを導入することで、これらが劇的に改善しました。
AWS Clean Roomsの基本的なアーキテクチャ
うちが実装した構成を図にしてみます。
graph TB
subgraph AdvertiserVPC ["Advertiser Account"]
A1["Customer Data Lake<br/>S3"]
A2["IAM Role<br/>Clean Rooms Access"]
end
subgraph PublisherVPC ["Publisher Account"]
P1["Audience Data<br/>S3"]
P2["IAM Role<br/>Clean Rooms Access"]
end
subgraph CleanRoomsService ["AWS Clean Rooms"]
CR1["Collaboration<br/>Space"]
CR2["Analysis<br/>Engine"]
CR3["Output Tables<br/>S3"]
CR4["Data Encryption<br/>KMS"]
end
subgraph AuditLayer ["Monitoring & Audit"]
AU1["CloudTrail"]
AU2["CloudWatch Logs"]
AU3["Config Rules"]
end
A1 -->|Share Encrypted| CR1
P1 -->|Share Encrypted| CR1
CR1 -->|Analyze| CR2
CR2 -->|Results| CR3
CR4 -->|Encrypt| CR3
CR1 -->|Log Access| AU1
CR2 -->|Monitor| AU2
CR3 -->|Config Check| AU3
AU1 -.->|Audit Trail| A2
AU1 -.->|Audit Trail| P2
ポイントは3つです。
1. 個別のAWSアカウント管理
広告主とメディア企業それぞれがIAMロールで権限管理するから、お互いのデータレイクに直接アクセスできません。そもそも見る権限がないわけです。
2. Clean Roomsがエンクリプション・マスキング処理を実行
生のデータは絶対に相手には見えず、Clean Roomsが指定したクエリだけで分析結果を返します。中間結果も暗号化されているので、サーバーレベルでも盗聴されない。
3. 監査ログが完全に記録される
誰がいつ何を分析したか、CloudTrailに全部残るから、後でコンプライアンスチェックするときめちゃくちゃ楽です。昔みたいに「あの時どうだったっけ…」って探り回る必要がないんですよね。
実装してわかった、Clean Roomsの3つのパターン
パターン1:ID マッチング
最初に導入したのが、ID マッチングです。広告主のcustomer_idと、メディア企業のuser_idをマッチングさせて、「この広告主の顧客は、このメディアにどのくらいいるのか」を調べたいってニーズでした。
-- Clean Rooms内で実行するクエリ
SELECT
COUNT(DISTINCT a.customer_id) as matched_customers,
p.content_category,
p.device_type
FROM advertiser_audience a
INNER JOIN publisher_audience p
ON a.email_hash = p.email_hash
AND a.customer_country = p.user_country
WHERE a.customer_type = 'premium'
AND p.visited_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY p.content_category, p.device_type
このクエリ、Clean Roomsの中で実行されるんです。広告主はメディアのraw dataを見られない。メディアも広告主のraw dataを見られない。見えるのは、このマッチング結果だけ。
正直に言うと、最初は「本当にセキュアなのかな」って疑ってました。でも CloudTrailで全アクセス記録が残って、どのクエリが実行されたか、結果は何だったか、全部監査可能。6ヶ月運用してみて、セキュリティと実用性のバランスが本当に取れていることに気づきました。
パターン2:オーディエンス セグメント分析
次は、セグメント単位での分析です。個人レベルのデータじゃなく、セグメント集計のみを許可するパターンですね。
{
"name": "audience_segment_analysis",
"description": "セグメント単位の分析のみ許可",
"configured_table_associations": [
{
"table_alias": "advertiser_segments",
"account_id": "XXXXXXXX",
"table_name": "customer_segments"
},
{
"table_alias": "publisher_analytics",
"account_id": "YYYYYYYY",
"table_name": "audience_segments"
}
],
"analysis_rules": {
"scalar": {
"allowed_analysis": "all",
"output_constraints": [
{
"column_name": "segment_id",
"type": "suppress",
"parameters": {}
},
{
"column_name": "count",
"type": "aggregate_constraint",
"parameters": {
"minimum_rows": 10
}
}
]
}
}
}
これ、重要なポイントが2つあります。
suppress制約
column_nameで指定したカラムは結果に含まれません。個人特定されるカラムはこれで隠します。個人識別子をバンバン消すのをいっぺんに設定しちゃえば、後からうっかりミスもない。
aggregate_constraint
最小行数を指定して、「10件未満の小さいセグメントは返さない」みたいなルール設定ができるんですよね。これは k-anonymity ってやつに対応するためです。
うちの場合、segment_idや個人情報カラムはすべてsuppress設定して、セグメントサイズと統計情報だけをメディア側に返すようにしました。これで、メディア企業は分析結果をビジネスに使えるけど、個人情報は一切見えない、という完全にプライバシー保護された状態になります。
パターン3:コホート分析
3番目は、時系列でのコホート分析パターン。「この広告主の新規顧客100人が、メディアのどこに来ているのか」みたいなやつです。
-- 新規顧客コホート
WITH advertiser_new_customers AS (
SELECT customer_id, signup_date
FROM advertiser_data.customers
WHERE signup_date >= DATE_TRUNC('month', CURRENT_DATE)
),
publisher_visits AS (
SELECT
user_id,
visited_at,
page_category,
session_duration_sec
FROM publisher_data.page_visits
WHERE visited_at >= CURRENT_DATE - INTERVAL '90 days'
)
SELECT
DATE_TRUNC('week', p.visited_at) as week,
p.page_category,
COUNT(DISTINCT a.customer_id) as cohort_size,
AVG(p.session_duration_sec) as avg_engagement
FROM advertiser_new_customers a
INNER JOIN publisher_visits p
ON a.customer_id = p.user_id
GROUP BY DATE_TRUNC('week', p.visited_at), p.page_category
ORDER BY week, page_category
これも Clean Roomsの中で実行します。広告主は「自社新規顧客がどこに来ているか」という聞きたい問いに対して、個人レベルのデータは一切見ずに、集計結果だけが得られる。メディア企業も「どの広告主の顧客が来ているか」は見えない。両社とも知りたいことは分かるけど、余計なことは見えない、っていう理想的な状況になるわけです。
パフォーマンス最適化で気づいたこと
ちょっと地味ですが、本番運用で大事だったのが、クエリパフォーマンスの話です。
うちの広告主側のデータレイク、dailyでアップデートされるのが2.5TBくらいあるんです。メディア側は3.5TB。こんなサイズのデータをClean Roomsで毎日分析すると、めちゃくちゃ遅くなります。正直、最初のパイロット段階では1回のクエリが10分以上かかってました。
対策としてやったことは2つです。
1. Pre-aggregation テーブルの作成
個別レコードで共有するんじゃなく、事前に日単位で集計したテーブルをClean Roomsに登録しました。
# 広告主側:customer_idごとに日単位で集計
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.appName("CleanRoomsPreAgg").getOrCreate()
# Raw customer data
raw_customers = spark.read.parquet("s3://advertiser-datalake/raw/customers/")
# Pre-aggregate to daily level
pre_agg = raw_customers.groupBy(
"date",
"customer_id",
"segment_id",
"country"
).agg(
F.sum("purchase_amount").alias("daily_spend"),
F.count("*").alias("transaction_count"),
F.avg("product_category").alias("primary_category")
)
# Register for Clean Rooms
pre_agg.write.mode("overwrite").parquet(
"s3://advertiser-datalake/clean-rooms/customer_daily_agg/"
)
これで、Clean Roomsが処理するデータ量が80%くらい減りました。クエリ実行時間も5分から30秒に短縮。かなりの改善です。
2. 共有カラムの制限
Clean Roomsに登録するテーブルで、本当に必要なカラムだけに絞りました。
-- Clean Rooms用:必要カラムのみ
CREATE TABLE advertiser_data.clean_rooms_customers AS
SELECT
customer_id,
email_hash,
country_code,
segment_id,
signup_date,
last_purchase_date,
customer_lifetime_value
FROM advertiser_data.customers
WHERE signup_date >= CURRENT_DATE - INTERVAL '2 years';
メディア側も同様に。結果として、わざわざしなくてもいい個人情報カラムが最初からないので、意図しないデータ流出も防げるし、パフォーマンスも良くなる、一石二鳥です。
運用していて困ったこと、そして対策
困ったこと1:データスキーマの変更
6ヶ月運用してみて、一番困ったのが、どちらかのテーブルスキーマが変わった時のハンドリングです。
広告主側でcustomer_ageカラムを追加したいってなったんですが、そこの権限管理が複雑です。Clean Roomsでは、カラムを追加する際に両社の合意が必要になります。
対策としては、スキーマ変更の前に双方でcontractを結んで、どのカラムを足すか、それがプライバシー的に大丈夫か、事前に審査してからデプロイするようにしました。
{
"configured_table_name": "customer_daily_agg",
"columns": [
{
"name": "customer_id",
"type": "string",
"analysis_rule_type": "aggregate"
},
{
"name": "customer_age",
"type": "int",
"analysis_rule_type": "aggregate",
"approved_by": "media_partner",
"approval_date": "2026-07-01"
}
]
}
こういう形で、スキーマ変更も監査対象にしておくと、後から「あ、このカラムはいつ追加されたっけ」って確認するのも簡単です。
困ったこと2:ID マッチング精度
メールアドレスのハッシュでマッチングさせたときに、精度が想定より低かったんです。広告主側のメールと、メディア側のメール、微妙に形式が違ったり、複数メールアドレスを持ってる人がいたり、あとは大文字小文字の統一がずれてたり。
マッチ率が最初は60%くらいだったので、「これ意味ないんじゃ…」って危機感持ちました。6ヶ月で気づいたのが、マッチング戦略を複数用意しておくことが大事ということ。
-- Strategy 1: Exact email hash match
SELECT
a.customer_id,
p.user_id,
'exact_email' as match_type
FROM advertiser_audience a
INNER JOIN publisher_audience p
ON a.email_hash = p.email_hash
UNION ALL
-- Strategy 2: Phone hash match (if email fails)
SELECT
a.customer_id,
p.user_id,
'phone_hash' as match_type
FROM advertiser_audience a
INNER JOIN publisher_audience p
ON a.phone_hash = p.phone_hash
AND a.email_hash IS NULL
UNION ALL
-- Strategy 3: First name + last initial + DOB (last resort)
SELECT
a.customer_id,
p.user_id,
'name_dob' as match_type
FROM advertiser_audience a
INNER JOIN publisher_audience p
ON a.first_name_hash = p.first_name_hash
AND a.last_initial = p.last_initial
AND a.dob_hash = p.dob_hash
複数マッチ戦略を持つことで、マッチ率が60%から82%に改善しました。メール持ってない人も取れるし、メールアドレス変わっちゃった人も電話番号でキャッチできる。こういう地味な工夫で精度ってかなり変わります。
Clean Rooms運用のメリット・デメリット完全比較
| 項目 | Clean Rooms | 従来方式(CSV共有) | 個別構築 |
|---|---|---|---|
| セットアップ時間 | 1週間 | 1日 | 2-3ヶ月 |
| 月額コスト | $2,000~5,000 | ほぼ0円 | $15,000~30,000 |
| セキュリティレベル | 非常に高 | 低い | 高い |
| コンプライアンス対応 | 自動化 | 手作業 | 手作業 |
| ID マッチング精度 | 70-85% | N/A | 95%+ |
| 分析柔軟性 | 中程度 | 低い | 非常に高 |
| 監査ログ完全性 | 100% | 50% | 80% |
| パートナー追加難易度 | 簡単 | 簡単 | 非常に難しい |
正直なところ、Clean Roomsはコストに見合う価値がある、という結論に達しました。特に、複数のパートナーとデータ共有する場合のスケーラビリティは優秀です。2社だけなら個別構築でもいいかもしれませんが、今後パートナーが増えることが確実なら、Clean Roomsに投資する方が絶対に楽です。
2026年時点でのClean Roomsの最新情報
2026年の時点で、Clean Roomsも大幅にアップデートされています。
1. マルチパーティ コラボレーション
もともとは2企業間での共有だけでしたが、今は3社以上での同時分析ができます。うちも最近、広告主+メディア+データ分析企業の3社で同時分析するパイロットを始めました。
テーブルの統合管理も簡単になって、「このカラムは広告主だけ見える」「このカラムは全社が見える」みたいな細かい権限設定が可能です。
2. 新しい分析エンジン
SQLだけじゃなく、Python/Rによる機械学習もClean Rooms内で実行できるようになりました。プライバシー保護しながら機械学習モデルを構築できるってのは、今までできなかったことです。
# Clean Rooms 内での機械学習実行
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
# Clean Rooms が提供するセキュアな dataframe
df_secure = clean_rooms.get_data("collaboration_id")
# 学習
X = df_secure[['feature_1', 'feature_2', 'feature_3']]
y = df_secure['target']
model = RandomForestClassifier()
model.fit(X, y)
# 結果は個人レベルではなく、モデル係数のみが返される
output = {
"feature_importance": model.feature_importances_,
"model_accuracy": model.score(X, y),
"auc": roc_auc_score(y, model.predict_proba(X)[:, 1])
}
個人データに基づいてモデルを作ってるんだけど、出力される情報はモデルの係数と精度指標だけ。こうすることで、「協調学習しながら個人データは守る」っていう矛盾した要件が実現します。
費用シミュレーション
うちの場合、実際の月額コスト構成がこんな感じです。
xychart-beta
title Clean Rooms 月額費用内訳
x-axis ["Data Processing", "Storage", "Collaboration Setup", "Analytics Queries"]
y-axis "Monthly Cost (USD)" 0 --> 5500
line [2100, 800, 500, 1200]
- Data Processing:$2,100 - 月10TBのデータ処理量で計算
- Storage:$800 - Clean Roomsテーブルの保存
- Collaboration Setup:$500 - 新規コラボレーション追加時
- Analytics Queries:$1,200 - 月200回の分析クエリ実行
合計$4,600/月。これまでCSV共有にかかってた人件費(データ抽出・検証・ドキュメント作成で月$3,000相当)を考えると、正味$1,600のコスト増。でも、セキュリティと監査の自動化を考えたら、マジで安い投資だと感じています。
データ品質管理との組み合わせ
Clean Roomsだけじゃなく、データ品質管理の仕組みとも連携させています。Clean Roomsに登録する前にデータ品質チェックを厳密にすることで、信頼できるデータだけが共有されるようにしました。
from great_expectations import validate
# Clean Rooms 登録前のデータ品質チェック
df = spark.read.parquet("s3://advertiser-datalake/clean-rooms/customer_daily_agg/")
validation_config = {
"expectations": [
{"type": "column_values_to_not_be_null", "column": "customer_id"},
{"type": "column_values_to_not_be_null", "column": "email_hash"},
{"type": "column_values_to_be_in_set", "column": "country_code", "value_set": country_codes},
{"type": "column_values_to_match_regex", "column": "email_hash", "regex": "^[a-f0-9]{64}$"},
]
}
result = validate(df, validation_config)
if not result["success"]:
raise Exception(f"Data quality check failed: {result['errors']}")
こうすることで、Clean Roomsにはクリーンなデータだけが登録されるので、分析精度も上がるし、データ品質の問題をめぐる議論も減ります。「このデータ変だよ」「いや、こっちのシステムの問題だ」みたいなやり取りが無くなるのは本当に楽ですよ。
まとめ
Clean Roomsを6ヶ月本番運用してわかったことは、以下の5点です。
1. プライバシー保護とビジネス価値の両立は可能
個人データを一切見せずに、集計・分析結果だけを共有できる仕組みは、GDPR/CCPA時代のデータ共有の正解だと感じました。規制を守りながらビジネスを進められるのって、実現できるんですよ。
2. ID マッチング精度は複数戦略で対策
単一のマッチング方式に頼るべからず。メール・電話・名前+DOBなど複数のPIIを活用することで、マッチ率を大幅に上げられます。最初から複数戦略を用意しておくと、後になって「精度が低い…」って焦ることもない。
3. Pre-aggregation が運用品質を劇的に改善
個別レコードで共有するのではなく、事前に集計したテーブルを登録することで、パフォーマンスとセキュリティが両立します。遅いシステムってユーザーに不信感を与えるから、ここは妥協しちゃダメです。
4. コンプライアンス自動化のコスト削減は無視できない
手作業でやっていた監査・記録作成が自動化されるので、人件費削減と運用品質向上が同時に起きます。監査の時期が来ると毎回バタバタしてたのに、今は CloudTrail を見りゃ終わり。本当に楽になった。
5. 複数パートナー対応時にスケーラビリティが活躍
2社間のデータ共有なら個別構築でもいいかもしれませんが、3社以上になるとClean Roomsの価値が一気に上がります。スキーマ統一、権限管理、監査ログ、全部自動化されるから、パートナーが増える度に新しい仕組みを作る必要がない。
次のステップとしては、Clean Rooms内での機械学習モデル構築を本格化させる予定です。現在パイロット段階ですが、プライバシー保護された状態で協調学習できるのは、まさに2026年のデータ共有のあるべき姿だと思います。
皆さんも複数企業とのデータ共有に困ってたら、ぜひ検討してみてください。セキュリティと実用性のバランスが、本当に取れてますよ。