#35│チャネルの「本当の貢献度」を遷移確率で計算するSQL

「最後に触ったチャネルが一番偉い」のは本当か

「どの広告チャネルに予算を配分すべきか」はマーケティング最大の難問の一つだ。

多くの企業が採用しているのはラストタッチアトリビューションだ。購買直前の接触チャネルに100%の成果を帰属させる。実装が簡単で直感的だが、重大な欠点がある。

ある顧客の購買経路を考えてみよう。

SEO → SNS広告 → メルマガ → 購買

ラストタッチでは「メルマガ」が100%の成果を取る。しかしSEOで最初にブランドを知り、SNS広告で関心を深め、メルマガが背中を押したとすれば、SEOもSNS広告も貢献しているはずだ。

マルコフ連鎖アトリビューションはこの問題を解決する。顧客の接触経路を「状態遷移のグラフ」として捉え、各チャネルを「除外したとき転換率がどれだけ下がるか」(削除効果)を計算することで、各チャネルの本当の貢献度を推定する。


マルコフ連鎖アトリビューションの仕組み

ステップ1:遷移行列を作る

顧客の接触経路から「チャネルAの次にチャネルBに移動する確率」を計算する。

SEO → SNS広告:40%
SEO → メルマガ:30%
SEO → 購買:10%
SEO → 離脱:20%

これを全チャネルで計算したのが遷移行列だ。

ステップ2:全経路での転換率を計算する

遷移行列を使ってシミュレーションし、「全チャネルが存在するとき」の全体転換率を計算する。

ステップ3:各チャネルを除外したときの転換率を計算する

「SEOを除外したとき」の転換率を計算する。全体転換率との差分が SEO の「削除効果(Removal Effect)」だ。

削除効果(SEO) = 全体転換率 − SEOを除外した場合の転換率

削除効果が大きいチャネルほど、そのチャネルがなければ多くの転換が失われる = 貢献度が高いと解釈する。


使用するテーブル

-- touchpoints(タッチポイントログ)
-- touchpoint_id  : タッチポイントID
-- customer_id    : 顧客ID
-- channel        : 接触チャネル('seo', 'sns', 'email', 'display', 'direct')
-- touched_at     : 接触日時(TIMESTAMP)
-- converted      : この接触後に購買したか(1 / 0)
-- session_id     : セッションID(複数タッチを1経路として束ねるキー)

-- journey_paths(変換経路テーブル ※touchpointsから集計して作る)
-- customer_id    : 顧客ID
-- path           : 接触経路(例:'seo > sns > email')
-- converted      : 最終的に購買したか(1 / 0)
-- touch_count    : タッチ数

STEP 1 ― 顧客ごとの接触経路を文字列として生成する

各顧客の接触履歴を「seo > sns > email」のような経路文字列にまとめる。

-- STEP1: 顧客ごとの接触経路を生成

WITH ordered_touchpoints AS (
    SELECT
        customer_id,
        channel,
        touched_at,
        converted,
        -- 顧客内での接触順序
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY touched_at ASC
        )  AS touch_order,
        -- 最終タッチかどうか
        CASE
            WHEN ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY touched_at DESC) = 1
            THEN 1 ELSE 0
        END  AS is_last_touch
    FROM touchpoints
    WHERE touched_at >= TIMESTAMP '2024-01-01 00:00:00'
    AND   touched_at <  TIMESTAMP '2025-01-01 00:00:00'
),
journey_paths AS (
    SELECT
        customer_id,
        -- 接触チャネルをカンマ区切りの経路に結合
        ARRAY_JOIN(
            ARRAY_AGG(channel ORDER BY touch_order ASC),
            ' > '
        )  AS path,
        MAX(converted)        AS converted,
        COUNT(channel)        AS touch_count
    FROM ordered_touchpoints
    GROUP BY customer_id
)
SELECT *
FROM journey_paths
ORDER BY touch_count DESC, converted DESC;

出力イメージ

customer_idpathconvertedtouch_count
C001seo > sns > email13
C002seo > seo > display > email14
C003sns > email02
C004direct11
C005seo > display > display > sns04

STEP 2 ― 遷移行列を計算する

経路文字列から「チャネルAの次にチャネルBが来る回数」を集計し、遷移確率を計算する。

状態の設定:通常のチャネルに加えて「Start(開始)」「Conversion(転換)」「Null(離脱)」を特殊状態として追加する。

-- STEP2: チャネル間の遷移確率を計算する

WITH ordered_touchpoints AS (
    SELECT
        customer_id,
        channel,
        touched_at,
        converted,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id ORDER BY touched_at ASC
        )  AS touch_order,
        COUNT(*) OVER (PARTITION BY customer_id)  AS total_touches
    FROM touchpoints
    WHERE touched_at >= TIMESTAMP '2024-01-01 00:00:00'
    AND   touched_at <  TIMESTAMP '2025-01-01 00:00:00'
),
-- 各タッチポイントに「次のチャネル」を付与
with_next AS (
    SELECT
        customer_id,
        channel                                          AS from_channel,
        -- 次のチャネル(最終タッチの次は Conversion または Null)
        COALESCE(
            LEAD(channel) OVER (
                PARTITION BY customer_id ORDER BY touch_order ASC
            ),
            -- 次がない場合(最終タッチ)
            CASE WHEN MAX(converted) OVER (PARTITION BY customer_id) = 1
                 THEN 'Conversion' ELSE 'Null' END
        )  AS to_channel,
        touch_order,
        total_touches
    FROM ordered_touchpoints

    UNION ALL

    -- Start状態からの遷移(各顧客の最初のタッチ)
    SELECT
        customer_id,
        'Start'  AS from_channel,
        channel  AS to_channel,
        0        AS touch_order,
        total_touches
    FROM ordered_touchpoints
    WHERE touch_order = 1
),
-- 遷移回数の集計
transition_counts AS (
    SELECT
        from_channel,
        to_channel,
        COUNT(*)  AS transition_count
    FROM with_next
    GROUP BY from_channel, to_channel
),
-- 遷移確率(各fromチャネルからの出口確率の合計で割る)
transition_matrix AS (
    SELECT
        from_channel,
        to_channel,
        transition_count,
        SUM(transition_count) OVER (PARTITION BY from_channel)  AS from_total,
        ROUND(
            transition_count * 1.0
            / SUM(transition_count) OVER (PARTITION BY from_channel)
        , 4)  AS transition_prob
    FROM transition_counts
)
SELECT *
FROM transition_matrix
ORDER BY from_channel, transition_prob DESC;

出力イメージ(遷移行列)

from_channelto_channeltransition_counttransition_prob
Startseo8,4120.4820
Startsns4,2180.2416
Startdirect2,8410.1627
Startemail1,2840.0735
Startdisplay7010.0402
seosns3,4120.3124
seoemail2,8410.2601
seoConversion1,2180.1115
seodisplay1,0840.0992
seoNull2,3840.2183
emailConversion2,1840.4812
emailNull2,3510.5188

email → Conversion の遷移確率が 0.4812 と高い。メルマガの「最後の一押し効果」が数字に出ている。


STEP 3 ― 全体転換率を遷移行列から計算する

遷移行列をもとに、Start から Conversion に到達する確率(全体転換率)をSQLで近似計算する。

厳密な計算は線形代数(逆行列)が必要だが、ここではモンテカルロシミュレーションの近似として「経路ごとの転換確率を積算する」アプローチを使う。

-- STEP3: 全経路の転換率を遷移確率から計算する

WITH journey_paths AS (
    -- STEP1の結果
    SELECT
        customer_id,
        ARRAY_JOIN(
            ARRAY_AGG(channel ORDER BY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY touched_at))
        , ' > ')  AS path,
        MAX(converted)  AS converted
    FROM touchpoints
    WHERE touched_at >= TIMESTAMP '2024-01-01 00:00:00'
    AND   touched_at <  TIMESTAMP '2025-01-01 00:00:00'
    GROUP BY customer_id
),
path_stats AS (
    SELECT
        path,
        COUNT(*)           AS path_count,
        SUM(converted)     AS conversions,
        ROUND(SUM(converted) * 1.0 / COUNT(*), 4)  AS path_conversion_rate
    FROM journey_paths
    GROUP BY path
),
-- 全体の転換率
overall_stats AS (
    SELECT
        SUM(path_count)              AS total_journeys,
        SUM(conversions)             AS total_conversions,
        ROUND(SUM(conversions) * 1.0 / SUM(path_count), 4)  AS overall_conversion_rate
    FROM path_stats
)
SELECT * FROM overall_stats;

出力イメージ

total_journeystotal_conversionsoverall_conversion_rate
17,4564,8120.2756

全体の転換率は27.56%だ。


STEP 4 ― 削除効果(Removal Effect)を計算する(完成版)

各チャネルを除外したとき、転換率がどれだけ低下するかを計算する。削除するとは「そのチャネルを含む経路からそのチャネルを取り除く」ことを意味し、経路が分断された場合(例:seo > email から email を除くと seo だけになる)は転換に至らなかったとみなす。

-- STEP4: チャネルを除外した場合の転換率と削除効果

WITH journey_paths AS (
    SELECT
        customer_id,
        ARRAY_JOIN(
            ARRAY_AGG(channel ORDER BY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY touched_at))
        , ' > ')  AS path,
        MAX(converted)  AS converted
    FROM touchpoints
    WHERE touched_at >= TIMESTAMP '2024-01-01 00:00:00'
    AND   touched_at <  TIMESTAMP '2025-01-01 00:00:00'
    GROUP BY customer_id
),
path_stats AS (
    SELECT path, COUNT(*) AS path_count, SUM(converted) AS conversions
    FROM journey_paths
    GROUP BY path
),
overall AS (
    SELECT SUM(conversions) * 1.0 / SUM(path_count)  AS overall_cr
    FROM path_stats
),
-- 各チャネルを除外したとき(そのチャネルを含む経路の転換数をゼロにする)
channel_removal AS (
    SELECT
        channel_name,
        -- そのチャネルを含まない経路の転換率
        SUM(CASE WHEN path NOT LIKE '%' || channel_name || '%' THEN conversions ELSE 0 END)
        * 1.0
        / SUM(path_count)  AS removal_cr
    FROM path_stats
    CROSS JOIN (
        SELECT 'seo'     AS channel_name UNION ALL
        SELECT 'sns'              UNION ALL
        SELECT 'email'            UNION ALL
        SELECT 'display'          UNION ALL
        SELECT 'direct'
    ) channels
    GROUP BY channel_name
)
SELECT
    cr.channel_name,
    ROUND(o.overall_cr, 4)                              AS overall_conversion_rate,
    ROUND(cr.removal_cr, 4)                             AS removal_conversion_rate,
    ROUND(o.overall_cr - cr.removal_cr, 4)              AS removal_effect,
    -- 削除効果を正規化してシェアに変換(全チャネルの削除効果の合計で割る)
    ROUND(
        (o.overall_cr - cr.removal_cr)
        / SUM(o.overall_cr - cr.removal_cr) OVER ()
    , 4)  AS attribution_share,
    ROUND(
        (o.overall_cr - cr.removal_cr)
        / SUM(o.overall_cr - cr.removal_cr) OVER ()
        * 100
    , 1)  AS attribution_pct
FROM channel_removal  cr
CROSS JOIN overall    o
ORDER BY attribution_pct DESC;

完成した出力イメージ

channel_nameoverall_crremoval_crremoval_effectattribution_pct
email0.27560.18910.086534.2%
seo0.27560.20840.067226.6%
sns0.27560.22410.051520.4%
direct0.27560.24910.026510.5%
display0.27560.25540.02028.0%
合計0.2519100%

アトリビューションモデルの比較

同じデータでラストタッチ・ファーストタッチ・均等分配・マルコフを並べると、モデルによる差が浮き彫りになる。

-- 3つのモデルを並べて比較する

WITH journey_paths AS (
    SELECT
        customer_id,
        ARRAY_JOIN(
            ARRAY_AGG(channel ORDER BY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY touched_at))
        , ' > ')  AS path,
        MAX(converted)  AS converted,
        -- ファーストタッチ
        MIN(channel)  AS first_channel,
        -- ラストタッチ(最後のチャネル)
        MAX(channel)  AS last_channel  -- 厳密にはLASTの取得が必要だが簡略化
    FROM touchpoints
    WHERE touched_at >= TIMESTAMP '2024-01-01 00:00:00'
    GROUP BY customer_id
),
converted_paths AS (
    SELECT * FROM journey_paths WHERE converted = 1
)
-- ラストタッチの集計
SELECT
    'ラストタッチ'  AS model,
    last_channel    AS channel,
    COUNT(*)        AS conversions,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1)  AS attribution_pct
FROM converted_paths
GROUP BY last_channel

UNION ALL

-- ファーストタッチの集計
SELECT
    'ファーストタッチ',
    first_channel,
    COUNT(*),
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1)
FROM converted_paths
GROUP BY first_channel

ORDER BY model, attribution_pct DESC;

3モデルの比較(マルコフは STEP4 の結果を追記)

modelchannelattribution_pct
ラストタッチemail58.2%
ラストタッチdirect24.1%
ラストタッチseo12.4%
ファーストタッチseo48.2%
ファーストタッチsns24.2%
ファーストタッチdirect16.3%
マルコフ削除効果email34.2%
マルコフ削除効果seo26.6%
マルコフ削除効果sns20.4%

ラストタッチでは email が 58.2% を独占するが、マルコフでは 34.2% に下がる。一方 seo はラストタッチの 12.4% からマルコフの 26.6% に上昇する。SEO の「入口としての貢献」がラストタッチでは過小評価されていたことが分かる。


実務での運用ヒント

① 経路の正規化(同じチャネルの連続を除去する)

seo > seo > email のような「同一チャネルの連続」は通常「同チャネルへの複数回接触」を意味する。これをそのまま使うと遷移行列が自己ループを持ち計算が複雑になる。seo > seo > email → seo > email のように連続する重複チャネルを除去する前処理を入れると精度が上がる。

② 接触期間の制限を設ける

「6ヶ月前の接触も今回の購買の貢献チャネルにカウントするか」という問題がある。一般的には「購買の30〜90日前以内の接触のみを経路に含める」というルックバックウィンドウを設定する。WHERE touched_at >= DATE_ADD('day', -90, purchase_date) で実装できる。

③ LIKE によるパターンマッチングの精度

STEP4 では path LIKE '%channel_name%' でチャネルを含む経路を検出した。seo を検索すると seo_brand のような別チャネルにもマッチしてしまう可能性がある。区切り文字(>)を含めて LIKE '% > seo > %' OR path LIKE 'seo > %' OR path LIKE '% > seo' と厳密に書くか、経路を配列として保持してより正確に検索する設計にするとよい。


まとめ

マルコフ連鎖アトリビューションの SQL 実装をまとめる。

  1. 顧客の接触履歴を ARRAY_JOIN + ARRAY_AGG で経路文字列に変換する
  2. LEAD を使って「次の状態」を付与し、Start → チャネル → Conversion/Null の遷移行列を作る
  3. 遷移確率 = 遷移回数 ÷ そのチャネルからの合計遷移数 として計算する
  4. 各チャネルを除外したとき(path LIKE '%channel%' の転換をゼロにしたとき)の転換率を計算する
  5. 全体転換率との差分(削除効果)を正規化してアトリビューションシェアとする

「最後に触ったチャネルが一番偉い」というラストタッチの偏見から脱して、SEO・SNS・メルマガそれぞれの本当の役割と貢献度を定量化することが、広告予算配分の精度を高める。このクエリは、予算の意思決定を「なんとなく」から「データに基づく根拠」へと変える道具だ。


MarTech Farmをもっと見る

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

続きを読む