はじめに
DatabricksのGenie Oneに、チャットの会話からドキュメントを作成できる機能が追加されています。
Genie One内のドキュメント | Databricks on AWS
Genie One のドキュメントを使用すると、インターフェースを離れることなく、チャットの応答を共有および編集可能なドキュメントに変換できます。
ポイントは、生成されたドキュメントに引用 (citation) が付与され、記述の根拠となったデータとクエリに遡れることです。加えてバージョン履歴が保持されるので、対話しながらドキュメントを育てていくワークフローが成立します。
ただ、この手の機能は手元のテーブルで「売上を集計して」とやっても面白さが伝わりません。そこで本記事では、ドキュメント生成が映えるように"物語"を仕込んだ合成データを用意し、以下を検証します。
- チャットから経営会議向けワンページャーを生成できるか
- 仕込んだストーリーを引用付きで発見できるか
- データにない因果を推測で書いてしまった場合に、フォローアップで検証・修正させられるか
結論から言うと、想像以上によくできています。仕込んでいなかった発見までしてくれました。
デモシナリオの設計
架空のコーヒーEC「夜明けコーヒー焙煎所」のFY2026 Q1 (2026年4月〜6月) の業務データを生成します。ドキュメントの公式サンプルプロンプトが「今四半期の主要な成果とリスクをまとめたワンページャー」なので、「ワンページャーを作って」と言うだけで成果とリスクの両方が引用付きで並ぶように、以下の4つのストーリーをデータに埋め込みます。
| # | 種別 | ストーリー | 発見に必要な分析 |
|---|---|---|---|
| 1 | 成果 | 5月の「新緑ブレンドフェア」でECのブレンド売上が急伸、ECは四半期目標を超過達成 (約115%) | チャネル別達成率、カテゴリー別月次売上 |
| 2 | リスク | 6月10日〜25日に関西で配送障害。チケット急増・関西売上減・「配送遅延」を理由とするサブスク解約が連動 | 3テーブル (support_tickets、sales_orders、subscriptions) の横断 |
| 3 | リスク | 6月から生豆価格高騰でシングルオリジンの原価率が約10pt上昇し粗利率が低下 | カテゴリー別粗利率の時系列 |
| 4 | リスク | 卸チャネルは目標未達 (約85%) | 目標と実績の結合 |
ストーリー2は複数テーブルを横断しないと全貌が見えない構造にしてあり、Genieのクロス集計能力を試すポイントになります。
生成するテーブルは以下の7つです。
| テーブル | 内容 |
|---|---|
| products | 商品マスタ (カテゴリー、価格、標準原価率) |
| customers | 顧客マスタ (地域、セグメント、獲得チャネル) |
| sales_orders | 受注明細 (約1.6万行、チャネル別、原価付き) |
| subscriptions | サブスクリプション契約と解約履歴 |
| support_tickets | サポートチケット |
| marketing_campaigns | マーケティングキャンペーン |
| monthly_targets | チャネル別月次売上目標 |
データ準備ノートブック
サーバーレスコンピュートおよびFree Editionで動作する構成です。工夫したポイントを先にまとめます。
- 目標は実績から逆算して生成: monthly_targetsは受注生成後に「EC 115%、店舗100%、卸85%」の達成率になるよう逆算するので、乱数の揺らぎに関係なくストーリー4が必ず成立します
- テーブル/カラムコメントを日本語で付与: 生成直後のテーブルには使用実績 (クエリ履歴やダッシュボード) がないため、Genieがテーブルの意味を理解する手がかりはコメントがほぼすべてです
- 日付の整合性を担保: 登録日より前に受注・チケット・契約が発生しないよう制御しています。合成データの粗をGenieに"発見"されるとドキュメントの焦点がぼやけるためです (後述しますが、初回はここで一本取られました)
セットアップ
dbutils.widgets.text("catalog", "main", "カタログ名")
dbutils.widgets.text("schema", "yoake_coffee", "スキーマ名")
catalog = dbutils.widgets.get("catalog")
schema = dbutils.widgets.get("schema")
spark.sql(f"CREATE SCHEMA IF NOT EXISTS {catalog}.{schema}")
spark.sql(f"USE {catalog}.{schema}")
import numpy as np
import pandas as pd
from datetime import date, timedelta
rng = np.random.default_rng(42)
Q_START = date(2026, 4, 1)
Q_END = date(2026, 6, 30)
DAYS = [(Q_START + timedelta(days=i)) for i in range((Q_END - Q_START).days + 1)]
# ストーリー用の期間定義
CAMPAIGN_START = date(2026, 5, 7) # 新緑ブレンドフェア
CAMPAIGN_END = date(2026, 5, 31)
INCIDENT_START = date(2026, 6, 10) # 関西配送障害
INCIDENT_END = date(2026, 6, 25)
COST_HIKE_START = date(2026, 6, 1) # 生豆価格高騰
商品マスタ
シングルオリジン5、ブレンド5、ドリップバッグ3、器具4、サブスク1の18商品です。カテゴリーごとに標準原価率を変えており、ドリップバッグとサブスクが高粗利になるようにしています。
products_data = [
# (product_id, product_name, category, unit_price, base_cost_ratio)
("P001", "エチオピア イルガチェフェ 200g", "シングルオリジン", 1980, 0.52),
("P002", "グアテマラ アンティグア 200g", "シングルオリジン", 1780, 0.50),
("P003", "ケニア ニエリ 200g", "シングルオリジン", 2180, 0.54),
("P004", "コロンビア ウイラ 200g", "シングルオリジン", 1680, 0.49),
("P005", "インドネシア マンデリン 200g", "シングルオリジン", 1880, 0.51),
("P006", "夜明けブレンド 200g", "ブレンド", 1380, 0.42),
("P007", "新緑ブレンド 200g", "ブレンド", 1480, 0.43),
("P008", "深煎りブレンド 200g", "ブレンド", 1280, 0.41),
("P009", "モーニングブレンド 500g", "ブレンド", 2680, 0.40),
("P010", "カフェインレスブレンド 200g", "ブレンド", 1580, 0.46),
("P011", "ドリップバッグ 夜明けブレンド 10袋", "ドリップバッグ", 1080, 0.38),
("P012", "ドリップバッグ アソート 15袋", "ドリップバッグ", 1680, 0.39),
("P013", "ドリップバッグ ギフトボックス 30袋", "ドリップバッグ", 3480, 0.37),
("P014", "セラミックドリッパー", "器具", 2480, 0.55),
("P015", "コーヒーサーバー 600ml", "器具", 3280, 0.58),
("P016", "ハンドミル", "器具", 5980, 0.60),
("P017", "ペーパーフィルター 100枚", "器具", 480, 0.45),
("P018", "月替わり定期便 200g×2", "サブスク", 2980, 0.44),
]
products_pdf = pd.DataFrame(
products_data,
columns=["product_id", "product_name", "category", "unit_price", "base_cost_ratio"],
)
顧客マスタ
2,000件のうち300件を四半期中の新規登録とし、5月 (フェア期間) にSNS広告経由の登録が集中するようにします。ここで重要なのが pick_customer ヘルパーで、登録日より前の日付でその顧客の受注やチケットが発生しないようにするための仕組みです。
N_CUSTOMERS = 2000
N_NEW_IN_QUARTER = 300 # 四半期中の新規登録顧客数
regions = ["北海道", "東北", "関東", "中部", "関西", "中国四国", "九州"]
region_weights = [0.05, 0.06, 0.38, 0.12, 0.24, 0.06, 0.09]
segments = ["個人", "法人"]
acq_channels = ["検索", "SNS広告", "紹介", "店舗", "イベント"]
customer_ids = [f"C{str(i).zfill(5)}" for i in range(1, N_CUSTOMERS + 1)]
cust_regions = rng.choice(regions, size=N_CUSTOMERS, p=region_weights)
cust_segments = rng.choice(segments, size=N_CUSTOMERS, p=[0.82, 0.18])
signup_dates = []
cust_acq = []
for i in range(N_CUSTOMERS):
if i < N_CUSTOMERS - N_NEW_IN_QUARTER:
# 既存顧客: 過去2年以内に登録
signup_dates.append(Q_START - timedelta(days=int(rng.integers(1, 720))))
cust_acq.append(str(rng.choice(acq_channels, p=[0.30, 0.28, 0.15, 0.17, 0.10])))
else:
# 四半期中の新規登録: 5月 (キャンペーン期間) に集中し、SNS広告経由が中心
month = int(rng.choice([4, 5, 6], p=[0.27, 0.46, 0.27]))
if month == 5:
day = int(rng.integers(7, 32)) # 新緑ブレンドフェア期間に集中
cust_acq.append(str(rng.choice(acq_channels, p=[0.15, 0.55, 0.10, 0.10, 0.10])))
else:
day = int(rng.integers(1, 29))
cust_acq.append(str(rng.choice(acq_channels, p=[0.30, 0.28, 0.15, 0.17, 0.10])))
signup_dates.append(date(2026, month, day))
customers_pdf = pd.DataFrame({
"customer_id": customer_ids,
"region": cust_regions,
"segment": cust_segments,
"acquisition_channel": cust_acq,
"signup_date": signup_dates,
})
# 受注・チケット生成用: 登録日でソートした顧客配列
def build_sorted_customers(pdf):
sub = pdf.sort_values("signup_date")
return sub["customer_id"].to_numpy(), np.array(sub["signup_date"].tolist(), dtype="datetime64[D]")
cust_sorted_by_region = {
r: build_sorted_customers(customers_pdf[customers_pdf["region"] == r]) for r in regions
}
cust_sorted_all = build_sorted_customers(customers_pdf)
def pick_customer(sorted_pair, d):
"""指定日時点で登録済みの顧客からランダムに1人選ぶ"""
ids, dates_arr = sorted_pair
n = int(np.searchsorted(dates_arr, np.datetime64(d), side="right"))
return str(rng.choice(ids[:n]))
受注明細
ストーリー1〜3をここに埋め込みます。キャンペーン期間はECの受注量とブレンドの選択確率を引き上げ、配送障害期間は関西向けECを減衰させ、6月以降のシングルオリジンは原価率を+0.10します。
price_map = dict(zip(products_pdf["product_id"], products_pdf["unit_price"]))
cost_ratio_map = dict(zip(products_pdf["product_id"], products_pdf["base_cost_ratio"]))
category_map = dict(zip(products_pdf["product_id"], products_pdf["category"]))
products_by_cat = {
c: products_pdf[products_pdf["category"] == c]["product_id"].tolist()
for c in products_pdf["category"].unique()
}
channels = ["EC", "店舗", "卸"]
# チャネル別のカテゴリー選択確率 (通常時)
cat_weights_by_channel = {
"EC": {"シングルオリジン": 0.28, "ブレンド": 0.34, "ドリップバッグ": 0.20, "器具": 0.10, "サブスク": 0.08},
"店舗": {"シングルオリジン": 0.30, "ブレンド": 0.38, "ドリップバッグ": 0.14, "器具": 0.18, "サブスク": 0.00},
"卸": {"シングルオリジン": 0.25, "ブレンド": 0.55, "ドリップバッグ": 0.20, "器具": 0.00, "サブスク": 0.00},
}
orders = []
order_seq = 1
for d in DAYS:
is_weekend = d.weekday() >= 5
in_campaign = CAMPAIGN_START <= d <= CAMPAIGN_END
in_incident = INCIDENT_START <= d <= INCIDENT_END
# チャネル別の日次受注件数
daily_counts = {
"EC": int(rng.normal(100, 10)),
"店舗": int(rng.normal(55, 8) * (1.35 if is_weekend else 1.0)),
"卸": int(rng.normal(14, 3)),
}
if in_campaign:
daily_counts["EC"] = int(daily_counts["EC"] * 1.35)
for channel, n_orders in daily_counts.items():
for _ in range(max(n_orders, 0)):
# 地域の決定 (配送障害期間は関西ECを減衰)
w = np.array(region_weights, dtype=float)
if channel == "EC" and in_incident:
kansai_idx = regions.index("関西")
w[kansai_idx] *= 0.6
w = w / w.sum()
region = rng.choice(regions, p=w)
customer_id = pick_customer(cust_sorted_by_region[region], d)
# カテゴリーの決定 (キャンペーン中のECはブレンドを強化)
cw = dict(cat_weights_by_channel[channel])
if channel == "EC" and in_campaign:
cw["ブレンド"] = cw["ブレンド"] * 2.0
cats = list(cw.keys())
probs = np.array([cw[c] for c in cats], dtype=float)
probs = probs / probs.sum()
category = rng.choice(cats, p=probs)
# 商品の決定 (キャンペーン中のECブレンドは新緑ブレンド P007 を優先)
if channel == "EC" and in_campaign and category == "ブレンド" and rng.random() < 0.5:
product_id = "P007"
else:
product_id = rng.choice(products_by_cat[category])
quantity = int(rng.integers(5, 25)) if channel == "卸" else int(rng.integers(1, 4))
unit_price = price_map[product_id]
if channel == "卸":
unit_price = int(unit_price * 0.65) # 卸掛率
# 原価 (6月以降のシングルオリジンは原価率+0.10)
cost_ratio = cost_ratio_map[product_id]
if category == "シングルオリジン" and d >= COST_HIKE_START:
cost_ratio += 0.10
unit_cost = int(price_map[product_id] * cost_ratio)
orders.append({
"order_id": f"O{str(order_seq).zfill(7)}",
"order_date": d,
"customer_id": customer_id,
"product_id": product_id,
"channel": channel,
"region": region,
"quantity": quantity,
"unit_price": unit_price,
"unit_cost": unit_cost,
"sales_amount": unit_price * quantity,
"cost_amount": unit_cost * quantity,
})
order_seq += 1
orders_pdf = pd.DataFrame(orders)
サブスクリプション
ベースラインの解約に加え、配送障害期間の関西顧客に「配送遅延」を理由とした解約を集中させます (ストーリー2)。契約開始日は登録日以降に制約します。
# 既存契約は四半期開始前に登録済みの顧客からサンプリング
legacy_customers = customers_pdf[customers_pdf["signup_date"] < Q_START]
subscriber_ids = rng.choice(legacy_customers["customer_id"], size=600, replace=False)
cust_region_map = dict(zip(customers_pdf["customer_id"], customers_pdf["region"]))
cust_signup_map = dict(zip(customers_pdf["customer_id"], customers_pdf["signup_date"]))
cancel_reasons_normal = ["価格", "飲みきれない", "好みに合わない", "その他"]
subscriptions = []
for i, cid in enumerate(subscriber_ids):
# 契約開始日は登録日以降・四半期開始前 (登録日との矛盾を防ぐ)
signup = cust_signup_map[cid]
max_offset = max((Q_START - signup).days, 1)
start_date = signup + timedelta(days=int(rng.integers(0, max_offset)))
region = cust_region_map[cid]
end_date = None
cancel_reason = None
# 配送障害による解約 (関西のサブスク顧客の約35%が期間中に解約)
if region == "関西" and rng.random() < 0.35:
end_offset = int(rng.integers(0, (INCIDENT_END - INCIDENT_START).days + 1))
end_date = INCIDENT_START + timedelta(days=end_offset)
cancel_reason = "配送遅延"
# ベースライン解約 (四半期で約7%)
elif rng.random() < 0.07:
end_offset = int(rng.integers(0, (Q_END - Q_START).days + 1))
end_date = Q_START + timedelta(days=end_offset)
cancel_reason = str(rng.choice(cancel_reasons_normal))
subscriptions.append({
"subscription_id": f"S{str(i + 1).zfill(5)}",
"customer_id": cid,
"plan_name": "月替わり定期便",
"monthly_fee": 2980,
"start_date": start_date,
"end_date": end_date,
"status": "解約" if end_date else "契約中",
"cancel_reason": cancel_reason,
})
# 四半期中の新規登録顧客の一部がサブスクを開始 (キャンペーンによる新規獲得のストーリー)
new_customers = customers_pdf[customers_pdf["signup_date"] >= Q_START]
new_sub_ids = rng.choice(new_customers["customer_id"], size=30, replace=False)
for cid in new_sub_ids:
signup = cust_signup_map[cid]
start_date = min(signup + timedelta(days=int(rng.integers(0, 8))), Q_END)
subscriptions.append({
"subscription_id": f"S{str(len(subscriptions) + 1).zfill(5)}",
"customer_id": cid,
"plan_name": "月替わり定期便",
"monthly_fee": 2980,
"start_date": start_date,
"end_date": None,
"status": "契約中",
"cancel_reason": None,
})
subscriptions_pdf = pd.DataFrame(subscriptions)
サポートチケット
日次ベースラインに加え、配送障害期間は関西発の「配送遅延」チケットを1日あたり15〜25件追加します。
ticket_categories = ["商品に関する問い合わせ", "注文変更", "品質不良", "配送遅延", "返品・交換", "その他"]
ticket_cat_weights = [0.30, 0.22, 0.10, 0.08, 0.15, 0.15]
severities = ["低", "中", "高"]
tickets = []
ticket_seq = 1
for d in DAYS:
# ベースラインチケット
n_base = int(rng.normal(8, 2))
for _ in range(max(n_base, 0)):
cid = pick_customer(cust_sorted_all, d)
cat = rng.choice(ticket_categories, p=ticket_cat_weights)
sev = rng.choice(severities, p=[0.55, 0.35, 0.10])
res_days = int(rng.integers(0, 4))
tickets.append({
"ticket_id": f"T{str(ticket_seq).zfill(6)}",
"created_date": d,
"customer_id": cid,
"region": cust_region_map[cid],
"category": cat,
"severity": sev,
"status": "解決済み" if d + timedelta(days=res_days) <= Q_END else "対応中",
"resolution_days": res_days,
})
ticket_seq += 1
# 配送障害期間の関西・配送遅延チケット
if INCIDENT_START <= d <= INCIDENT_END:
n_incident = int(rng.integers(15, 26))
for _ in range(n_incident):
cid = pick_customer(cust_sorted_by_region["関西"], d)
sev = rng.choice(severities, p=[0.10, 0.45, 0.45])
res_days = int(rng.integers(2, 10))
tickets.append({
"ticket_id": f"T{str(ticket_seq).zfill(6)}",
"created_date": d,
"customer_id": cid,
"region": "関西",
"category": "配送遅延",
"severity": sev,
"status": "解決済み" if d + timedelta(days=res_days) <= Q_END else "対応中",
"resolution_days": res_days,
})
ticket_seq += 1
tickets_pdf = pd.DataFrame(tickets)
キャンペーンマスタ
意図的に「効果を仕込んだキャンペーン」と「効果を仕込んでいないキャンペーン」を混ぜています。後述しますが、これがデモの面白い伏線になりました。
campaigns_pdf = pd.DataFrame([
{
"campaign_id": "CP001",
"campaign_name": "新緑ブレンドフェア",
"channel": "EC",
"target_category": "ブレンド",
"start_date": CAMPAIGN_START,
"end_date": CAMPAIGN_END,
"budget": 3500000,
"description": "新商品「新緑ブレンド」を中心としたEC限定の春キャンペーン。SNS広告とメールを併用。",
},
{
"campaign_id": "CP002",
"campaign_name": "母の日ギフトキャンペーン",
"channel": "EC",
"target_category": "ドリップバッグ",
"start_date": date(2026, 4, 20),
"end_date": date(2026, 5, 10),
"budget": 1200000,
"description": "ドリップバッグギフトボックスを訴求する母の日向けキャンペーン。",
},
])
月次目標 (実績から逆算)
orders_pdf["order_month"] = orders_pdf["order_date"].map(lambda d: d.strftime("%Y-%m"))
actual_by_month_channel = (
orders_pdf.groupby(["order_month", "channel"])["sales_amount"].sum().reset_index()
)
achievement_rates = {"EC": 1.15, "店舗": 1.00, "卸": 0.85}
targets = []
for _, row in actual_by_month_channel.iterrows():
rate = achievement_rates[row["channel"]]
# 月ごとに±3%のゆらぎを加える
noise = rng.uniform(0.97, 1.03)
target_amount = int(row["sales_amount"] / (rate * noise) // 10000 * 10000)
targets.append({
"target_month": row["order_month"],
"channel": row["channel"],
"target_amount": target_amount,
})
targets_pdf = pd.DataFrame(targets)
Deltaテーブルへの書き込みとコメント付与
Genieがテーブルの意味を理解できるよう、テーブルコメントと主要カラムのコメントを日本語で付与します。前述の通り、生成直後のテーブルではこれがGenieの主な手がかりになります。
orders_write_pdf = orders_pdf.drop(columns=["order_month"])
tables = {
"products": products_pdf,
"customers": customers_pdf,
"sales_orders": orders_write_pdf,
"subscriptions": subscriptions_pdf,
"support_tickets": tickets_pdf,
"marketing_campaigns": campaigns_pdf,
"monthly_targets": targets_pdf,
}
for name, pdf in tables.items():
sdf = spark.createDataFrame(pdf)
sdf.write.mode("overwrite").option("overwriteSchema", "true").saveAsTable(name)
table_comments = {
"products": "コーヒーEC「夜明けコーヒー焙煎所」の商品マスタ。カテゴリー、税込単価、標準原価率を保持する。",
"customers": "顧客マスタ。地域、セグメント (個人/法人)、獲得チャネル、登録日を保持する。",
"sales_orders": "受注明細。1行が1受注に対応し、チャネル (EC/店舗/卸)、地域、売上金額、原価金額を保持する。粗利は sales_amount - cost_amount で計算する。",
"subscriptions": "コーヒー定期便のサブスクリプション契約。解約時は end_date と cancel_reason が設定される。",
"support_tickets": "カスタマーサポートのチケット。カテゴリー (配送遅延、品質不良など) と深刻度、解決日数を保持する。",
"marketing_campaigns": "マーケティングキャンペーンのマスタ。実施期間、対象チャネル、対象カテゴリー、予算を保持する。",
"monthly_targets": "チャネル別の月次売上目標。sales_orders の実績と突き合わせて達成率を計算する。",
}
for table, comment in table_comments.items():
spark.sql(f"COMMENT ON TABLE {catalog}.{schema}.{table} IS '{comment}'")
カラムコメントの付与部分は長くなるので省略しますが、ALTER TABLE ... ALTER COLUMN ... COMMENT で sales_amount の計算方法や cancel_reason の意味などを記述しています。
ワンページャーを生成してみる
Genie Oneのチャットに以下のプロンプトを投げます。
takaakiyayoi_catalog.yoake_coffee スキーマにあるコーヒーEC「夜明けコーヒー焙煎所」のデータを使って、今四半期 (2026年4月〜6月) の主要な成果とリスクをまとめた経営会議向けのワンページャーをドキュメントとして作成してください。
1点補足すると、プロンプトにスキーマと会社名を明示しているのは、生成直後のテーブルには使用実績がないためです。Genie Oneはナレッジスニペットを生成元・使用頻度・鮮度に基づくオーソリティースコアでランク付けして参照先を決めるので、履歴のない新規テーブルは素のプロンプトだと見つけてもらいにくい可能性があります。最初の1問でスキーマを伝えれば、以降は会話の文脈で引き継がれます。
Genieは思考プロセスの中でスキーマの全テーブルを調べ、複数のSQLクエリを実行して集計し、キャンバスペインにドキュメントを生成しました。
生成されたドキュメントの構成は以下の通りです。
- エグゼクティブサマリー: 売上高8,346万円、粗利率47.0%、ECが+16.5%で牽引する一方、卸が▲15.2%未達、6月の配送遅延問題への早急な対策が必要
- 主要KPI一覧 (売上、粗利、受注件数、新規顧客獲得300名、サブスク期首600→期末530、MRRなど)
- 成果ハイライト: EC達成率116.5% (SNS広告経由の新規獲得108名が寄与)、高粗利カテゴリの構成、地域別分析
- リスクと課題: 配送遅延チケット急増 (6月554件、うち配送遅延352件、平均解決日数5.1日 vs 通常1.4日)、解約100件中63件が配送遅延起因、卸チャネル3ヶ月連続未達
- 次四半期に向けたアクションアイテム
仕込んだストーリー1、2、4が引用付きで回収されています。ストーリー2 (配送障害) はsupport_tickets、subscriptions、sales_ordersを横断して「チケット急増」「解約理由の63%」「チャーン率12.4%」まで整理されており、クロス集計は期待通りでした。各記述の引用をクリックすると、根拠となったクエリとテーブルに遡れます。
推測で書かれた因果を検証させる
ここからが本記事のハイライトです。生成されたドキュメントの成果セクションに、こんな記述がありました。
5月の売上3,035万円は母の日ギフト需要等により四半期内最高。
もっともらしいのですが、データ上の5月ピークの主因は新緑ブレンドフェアであり、この時点でGenieはmarketing_campaignsテーブルを参照していません (データソースの脚注にも含まれていませんでした)。つまりこれは、売上ピークという事実に対してGenieが一般常識で埋めた推測です。ドキュメントに引用が付く価値は、まさにこういう記述を見分けられることにあります。
そこでフォローアップを投げます。
marketing_campaignsテーブルも確認して、5月の売上ピークとECの好調の要因を検証し、成果セクションの記述を裏付けのあるものに更新してください。
結果が見事でした。
- 「母の日ギフト需要等により」の記述が打ち消し線となり、「新緑ブレンドフェア (5/7〜5/31) と母の日ギフトキャンペーン (4/20〜5/10) の2施策が重複した5月上旬〜中旬にEC日次売上が50〜63万円/日に達し」という裏付けのある記述に更新
- 新緑ブレンドフェアの効果を定量化: ブレンドEC売上 4月338万円 → 5月592万円 (+75%)、日次売上はキャンペーン前11.1万円/日 → 実施中21.2万円/日 (+90%)
- ドキュメントは変更履歴付きで編集され、「すべての編集を承認」で確定するワークフロー
そして予想外に面白かったのが母の日キャンペーンの検証結果です。
母の日キャンペーンはドリップバッグの日次売上に有意な変化がなく (前後とも8.0万円/日)、認知拡大には寄与したものの直接的な売上リフトは確認できませんでした。
実はデータ生成ロジックでは母の日キャンペーン (CP002) に売上ブーストを一切仕込んでいません。つまりこの結論はデータに対して完全に正確です。キャンペーンテーブルに施策が存在するからといって効果をでっち上げず、日次売上を突き合わせて「リフトなし」と判定しました。効果のあった施策となかった施策を分析で切り分けたわけで、意図せず良い検証材料になりました。
未検出のリスクを追記させる
仕込んだ4ストーリーのうち、ストーリー3 (シングルオリジンの原価高騰) だけは初版で検出されていませんでした。カテゴリ別粗利率の表でシングルオリジンが37.6%と他より低いことまでは出ていたのですが、時系列の掘り下げには至っていませんでした。これも追記を依頼します。
シングルオリジンの粗利率を月次で分析し、6月の変化があればリスクセクションに追記してください。
返ってきた分析は期待を超えていました。
- 平均単位原価が978円 → 1,160円 (+18.7%)、粗利率が41.9% → 29.6% (▲12.3pt) と月次推移チャート付きで定量化
- 商品別テーブルで、全5商品が同時に+18〜20%上昇していることを示し、「生豆の国際相場上昇が背景と推察される」と正しい説明に到達 (原価率+0.10という仕込みをカテゴリー一律で入れたので、"全商品同時"がシグナルになっています)
- このまま継続した場合の減益インパクト (前期比▲約940万円) を試算し、アクションアイテムに価格改定・調達先多様化を追記
さらに、こちらが仕込んでいなかった発見までしてくれました。
卸チャネルでは粗利率が5.3%まで低下し、ほぼ利益が出ていない状態です。
これはデータ生成ロジックの帰結です。卸の販売単価は定価の65%掛けにしている一方、原価は定価ベースで計算しているため、原価高騰の打撃は卸チャネルに最も強く出ます。独立に仕込んだ「卸の目標未達」と「原価高騰」という2つのリスクが、「卸の収益性悪化」という1つの物語に合流した形で、Genieはこの二次効果をクロス集計で掘り当てました。合成データであっても、ロジックの整合性さえ保っていれば創発的な発見が起こる、というのは面白い学びでした。
なお、この編集で解約理由テーブルが新セクションの前後に重複するという編集アーティファクトが発生しました。ドキュメントはキャンバス上で直接編集できるので、この程度は手で消せば済みますし、「重複を解消して」と依頼しても対応してくれます。
バージョン履歴と共有
ここまでのやり取りで、ドキュメントは「初版 (推測を含む)」→「キャンペーン検証後」→「原価リスク追記後」と3世代を経ています。ドキュメントはバージョン履歴を保持するため、キャンバスペインから以前のバージョンを確認できます。レビュープロセスで「どの記述がいつどう変わったか」を追えるのは、経営会議向け資料のような用途では実用的です。
仕上がったドキュメントは右上の共有ボタンから、ユーザー、グループ、サービスプリンシパルを指定して、またはリンクで共有できます。
まとめ
Genie Oneのドキュメント機能を、ストーリーを仕込んだ合成データで一通り試しました。学びを整理します。
機能面
- チャットからワンページャーを生成し、引用・変更履歴付き編集・バージョン履歴・共有までワークフローが一通り揃っている
- 複数テーブルを横断するクロス集計 (チケット×解約×売上) も問題なくこなす
- 効果のないキャンペーンについて「売上リフトは確認できない」と正しく否定でき、存在するだけの施策に効果をでっち上げない
運用面
- 生成直後のテーブルは使用実績がないため、最初のプロンプトでスキーマや事業名などのコンテキストを明示するのが確実
- テーブル/カラムコメントは日本語でしっかり書く価値がある
- 引用が付いていない、あるいは参照テーブルが不足している記述は推測の可能性がある。「母の日ギフト需要等により」のような、もっともらしい因果の説明こそ引用クリックで検証すべき
- 推測を見つけたら、参照すべきテーブルを指定して検証・更新を依頼すれば、変更履歴付きで修正される
「AIが書いたレポートは信用できるのか」という問いに対して、引用で根拠に遡れて、疑わしい箇所は対話で検証・修正できる、というのがこの機能の答え方なのだと思います。データ準備ノートブックは本記事のコードをつなげればそのまま動くので、ぜひ手元のワークスペースで試してみてください。
参考資料
- Genie One内のドキュメント | Databricks on AWS
- Genie Oneを使用する | Databricks on AWS
- Genie Oneでチャットする | Databricks on AWS
- Genie One、Genie Agents、Genie Ontologyのご紹介 | Databricks Blog








