本文へスキップ
martechfarmDATA & TECHNOLOGY DOCS

効果検証SQL 内を検索 · 全記事に戻す

効果検証SQL

7 件

タイトルを選ぶと本文が開きます

アップリフトモデリングをSQLで近似する方法|施策が効く顧客だけを抽出する

マーケティングチャネル貢献度をSQLで計算する方法|遷移確率でアトリビューション分析

差分の差分法をSQLで実装する方法|キャンペーン効果を因果推論で検証する

ファネル分析をSQLで行う方法|30分以内のカゴ投入から購買を正確に測る

SQLでランダムサンプリングとA/Bテスト割当を行う方法

クーポン施策の効果をSQLで検証する方法|割引の売上貢献を正しく測る

メルマガの購買転換率をSQLで計測する方法|配信効果を可視化するクエリ

本文
記事一覧へ ↑

メルマガの購買転換率をSQLで計測する方法|配信効果を可視化するクエリ


「メルマガを送ったら売上が上がった」は本当か

メルマガを配信した翌日に注文が増えると、担当者は「効果があった」と判断しがちだ。しかし少し立ち止まって考えてほしい。

  • その注文は、メルマガを受け取ったから起きたのか
  • それとも、メルマガとは無関係にそもそも買うつもりだった顧客が注文したのか
  • あるいは、配信日がたまたま週末でもともと注文が多い曜日だったのか

この問いに答えるためには、「誰にメルマガを送ったか」と「誰が購買したか」を突き合わせるクエリが必要だ。

今回は、メルマガの配信ログと購買ログをJOINして転換率を計測するクエリを作る。「メルマガを受け取った人」と「受け取っていない人」で購買率を比較することで、施策の真の効果に近づくことができる。


アトリビューションの考え方

SQLに入る前に、「どこまでをメルマガの効果と見なすか」を決めておく必要がある。これをアトリビューション(貢献度の帰属)と呼ぶ。

今回は最もシンプルな定義を使う。

メルマガ配信から72時間以内に購買した顧客を「転換した」と見なす

72時間(3日間)という窓は業界でよく使われる目安だ。短すぎると「開封して検討した顧客」を取りこぼし、長すぎると「メルマガと無関係な購買」を巻き込んでしまう。自社の顧客行動データを見ながら調整してほしい。

指標定義
配信数メルマガを送った顧客の数
転換数配信から72時間以内に購買した顧客の数
転換率転換数 ÷ 配信数
非受信者購買率配信しなかった顧客が同期間に購買した割合(比較対象)

使用するテーブル

今回は新たに2つのテーブルを使う。

-- メルマガ配信ログテーブル(mail_send_log)
-- send_id      : 配信ID(主キー)
-- campaign_id  : キャンペーンID
-- campaign_name: キャンペーン名
-- customer_id  : 配信対象の顧客ID
-- sent_at      : 配信日時(TIMESTAMP型)
send_idcampaign_idcampaign_namecustomer_idsent_at
S0001C001春の新作案内C0012024-03-01 10:00:00
S0002C001春の新作案内C0022024-03-01 10:00:00
S0003C001春の新作案内C0032024-03-01 10:00:00
S0004C002会員限定セールC0012024-04-15 11:00:00
……………
-- 注文テーブル(orders)
-- order_id     : 注文ID
-- customer_id  : 顧客ID
-- order_date   : 注文日(DATE型)
-- ordered_at   : 注文日時(TIMESTAMP型)
-- total_amount : 注文金額
-- status       : 'completed' / 'cancelled' など

日付型とタイムスタンプ型の使い分け

sent_at は時刻まで持つ TIMESTAMP 型だ。72時間ウィンドウのような時刻精度の計算には TIMESTAMP 型が必要なので、orders テーブルにも ordered_at(TIMESTAMP型)カラムがあることを前提にしている。order_date(DATE型)しか持っていない場合は、CAST(order_date AS TIMESTAMP) で代用できるが、時刻が 00:00:00 になるため72時間の計算が日をまたぐ場合に注意が必要だ。


STEP 1 ― 配信リストと購買ログを突き合わせる

まず特定キャンペーンの配信リストと、その後の購買を結びつける。

-- STEP1: 配信顧客と購買の突き合わせ

WITH campaign_sends AS (
    -- 対象キャンペーンの配信リスト
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        sent_at
    FROM mail_send_log
    WHERE campaign_id = 'C001'  -- 分析対象のキャンペーンIDを指定
),
post_send_orders AS (
    -- 配信後に購買した記録を引っ張る
    SELECT
        s.customer_id,
        s.campaign_id,
        s.campaign_name,
        s.sent_at,
        o.order_id,
        o.ordered_at,
        o.total_amount,
        -- 配信から購買までの時間(時間単位)
        DATE_DIFF('hour', s.sent_at, o.ordered_at)  AS hours_after_send
    FROM campaign_sends  s
    LEFT JOIN orders     o
        ON  s.customer_id = o.customer_id
        AND o.status      = 'completed'
        -- 配信後から72時間以内の購買のみ
        AND o.ordered_at >= s.sent_at
        AND o.ordered_at <  s.sent_at + INTERVAL '72' HOUR
)
SELECT *
FROM post_send_orders
ORDER BY customer_id, ordered_at;

出力イメージ

customer_idcampaign_namesent_atorder_idordered_attotal_amounthours_after_send
C001春の新作案内2024-03-01 10:00102412024-03-02 14:328,40028
C002春の新作案内2024-03-01 10:00NULLNULLNULLNULL
C003春の新作案内2024-03-01 10:00102982024-03-01 21:154,20011
C003春の新作案内2024-03-01 10:00104122024-03-03 09:4012,60047
…………………

C001は配信から28時間後に購買した(転換)。C002は72時間以内に購買なし(未転換)。C003は配信後に2回購買している。LEFT JOINを使っているので、購買しなかった顧客もNULLとして残り、転換率の分母に含まれる。


STEP 2 ― 顧客単位に集約して転換フラグを立てる

STEP1では1顧客が複数行になる場合がある(72時間以内に複数回購買した場合)。転換率の計算では「転換した/しなかった」の2択なので、顧客単位に集約する。

-- STEP2: 顧客単位に集約して転換フラグを付与

WITH campaign_sends AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        sent_at
    FROM mail_send_log
    WHERE campaign_id = 'C001'
),
post_send_orders AS (
    SELECT
        s.customer_id,
        s.campaign_id,
        s.campaign_name,
        s.sent_at,
        o.order_id,
        o.ordered_at,
        o.total_amount,
        DATE_DIFF('hour', s.sent_at, o.ordered_at)  AS hours_after_send
    FROM campaign_sends  s
    LEFT JOIN orders     o
        ON  s.customer_id = o.customer_id
        AND o.status      = 'completed'
        AND o.ordered_at >= s.sent_at
        AND o.ordered_at <  s.sent_at + INTERVAL '72' HOUR
),
customer_conversion AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        sent_at,
        -- 72時間以内に1件でも購買があれば転換フラグ=1
        CASE WHEN COUNT(order_id) > 0 THEN 1 ELSE 0 END  AS is_converted,
        -- 転換した場合の購買件数・購買金額
        COUNT(order_id)                                    AS order_count,
        COALESCE(SUM(total_amount), 0)                     AS total_amount,
        -- 初回転換までの時間
        MIN(hours_after_send)                              AS hours_to_first_purchase
    FROM post_send_orders
    GROUP BY
        customer_id,
        campaign_id,
        campaign_name,
        sent_at
)
SELECT *
FROM customer_conversion
ORDER BY is_converted DESC, hours_to_first_purchase;

出力イメージ

customer_idcampaign_nameis_convertedorder_counttotal_amounthours_to_first_purchase
C001春の新作案内118,40028
C003春の新作案内1216,80011
C012春の新作案内115,6004
C002春の新作案内000NULL
C008春の新作案内000NULL
………………

STEP 3 ― キャンペーン単位の転換率サマリを出す(完成版)

顧客単位のフラグが揃ったので、キャンペーン全体のサマリを出す。

-- STEP3: キャンペーン転換率サマリ(完成版)

WITH campaign_sends AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        sent_at
    FROM mail_send_log
    WHERE campaign_id = 'C001'
),
post_send_orders AS (
    SELECT
        s.customer_id,
        s.campaign_id,
        s.campaign_name,
        s.sent_at,
        o.order_id,
        o.ordered_at,
        o.total_amount,
        DATE_DIFF('hour', s.sent_at, o.ordered_at)  AS hours_after_send
    FROM campaign_sends  s
    LEFT JOIN orders     o
        ON  s.customer_id = o.customer_id
        AND o.status      = 'completed'
        AND o.ordered_at >= s.sent_at
        AND o.ordered_at <  s.sent_at + INTERVAL '72' HOUR
),
customer_conversion AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        CASE WHEN COUNT(order_id) > 0 THEN 1 ELSE 0 END  AS is_converted,
        COUNT(order_id)                                    AS order_count,
        COALESCE(SUM(total_amount), 0)                     AS total_amount,
        MIN(hours_after_send)                              AS hours_to_first_purchase
    FROM post_send_orders
    GROUP BY customer_id, campaign_id, campaign_name, sent_at
)
SELECT
    campaign_id,
    campaign_name,

    -- 配信数
    COUNT(customer_id)                                     AS send_count,

    -- 転換数・転換率
    SUM(is_converted)                                      AS converted_count,
    ROUND(SUM(is_converted) * 100.0 / COUNT(customer_id), 2)
                                                           AS conversion_rate_pct,

    -- 転換顧客の購買金額
    SUM(total_amount)                                      AS attributed_sales,
    ROUND(AVG(CASE WHEN is_converted = 1 THEN total_amount END), 0)
                                                           AS avg_sales_per_converter,

    -- 配信1件あたりの売上貢献(ROAS的な指標)
    ROUND(SUM(total_amount) * 1.0 / COUNT(customer_id), 0)
                                                           AS sales_per_send,

    -- 転換までの中央値時間(Prestoの場合)
    APPROX_PERCENTILE(hours_to_first_purchase, 0.5)        AS median_hours_to_purchase

FROM customer_conversion
GROUP BY campaign_id, campaign_name;

出力イメージ

campaign_idcampaign_namesend_countconverted_countconversion_rate_pctattributed_salessales_per_send
C001春の新作案内4,82072415.028,412,0001,745

配信4,820件のうち724件(15%)が72時間以内に購買した。1配信あたりの売上貢献は1,745円だ。


STEP 4 ― 複数キャンペーンを横並びで比較する

実務では「どのキャンペーンが効いたか」を比較したい。campaign_id = 'C001' の絞り込みを外すだけで全キャンペーンが比較できる。

-- STEP4: 全キャンペーンの転換率比較

WITH all_sends AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        sent_at
    FROM mail_send_log
),
post_send_orders AS (
    SELECT
        s.customer_id,
        s.campaign_id,
        s.campaign_name,
        o.order_id,
        o.total_amount
    FROM all_sends  s
    LEFT JOIN orders  o
        ON  s.customer_id = o.customer_id
        AND o.status      = 'completed'
        AND o.ordered_at >= s.sent_at
        AND o.ordered_at <  s.sent_at + INTERVAL '72' HOUR
),
customer_conversion AS (
    SELECT
        customer_id,
        campaign_id,
        campaign_name,
        CASE WHEN COUNT(order_id) > 0 THEN 1 ELSE 0 END  AS is_converted,
        COALESCE(SUM(total_amount), 0)                     AS total_amount
    FROM post_send_orders
    GROUP BY customer_id, campaign_id, campaign_name
)
SELECT
    campaign_id,
    campaign_name,
    COUNT(customer_id)                                          AS send_count,
    SUM(is_converted)                                          AS converted_count,
    ROUND(SUM(is_converted) * 100.0 / COUNT(customer_id), 2)  AS conversion_rate_pct,
    SUM(total_amount)                                          AS attributed_sales,
    ROUND(SUM(total_amount) * 1.0 / COUNT(customer_id), 0)    AS sales_per_send
FROM customer_conversion
GROUP BY campaign_id, campaign_name
ORDER BY conversion_rate_pct DESC;

出力イメージ

campaign_namesend_countconverted_countconversion_rate_pctattributed_salessales_per_send
会員限定セール2,14042719.956,840,0003,196
春の新作案内4,82072415.028,412,0001,745
誕生日クーポン1,28017913.982,512,0001,963
再入荷通知8909811.011,176,0001,321

会員限定セールは転換率・1配信あたり売上ともに最も高い。春の新作案内は配信数が多く総売上は大きいが、効率は劣る。このような比較があれば、次の配信計画や予算配分の議論が具体的になる。


実務での注意点

① 同一顧客への重複配信に気をつける

同じ顧客に同じキャンペーンを複数回配信していると、mail_send_log に同一顧客の行が複数存在する。その場合、campaign_sends のCTEで SELECT DISTINCT customer_id にするか、ROW_NUMBER() で最初の配信だけを取るなどの重複排除が必要だ。

② 「メルマガなしでも買っていたか」という問題

今回計測した転換率は「送ったら何%が買ったか」だ。しかし「送らなくても買っていた人」が含まれている可能性は排除できない。厳密な効果測定には、配信した顧客群(処置群)と配信しなかった顧客群(対照群)を比較するABテスト設計が必要だ。今回のクエリはその第一歩として「まず基準値を把握する」目的で使ってほしい。

③ Treasure DataでのINTERVAL構文

Treasure DataのPrestoでは INTERVAL '72' HOUR の代わりに sent_at + (72 * 3600) のようにUNIXタイムスタンプ演算で書くほうが安定するケースがある。sent_at がTD_TIME形式(UNIX秒)で格納されている場合は ordered_at_unix <= sent_at_unix + 72 * 3600 と書き換えよう。


まとめ

今回のクエリの骨格を振り返る。

  1. mail_send_log から対象キャンペーンの配信リストを作る
  2. orders を LEFT JOIN(配信後72時間以内という時間条件付き)で結合する
  3. 顧客単位に集約してis_converted フラグを立てる
  4. SUM(is_converted) / COUNT(customer_id) で転換率を計算する

LEFT JOINと時間条件の組み合わせがこのクエリの核心だ。「配信した全員を分母に保ちながら、転換した人だけを数える」という構造を体に馴染ませておくと、さまざまな施策効果測定に応用できる。


MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む