「最後に触ったチャネルが一番偉い」のは本当か
「どの広告チャネルに予算を配分すべきか」はマーケティング最大の難問の一つだ。
多くの企業が採用しているのはラストタッチアトリビューションだ。購買直前の接触チャネルに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_id | path | converted | touch_count |
|---|---|---|---|
| C001 | seo > sns > email | 1 | 3 |
| C002 | seo > seo > display > email | 1 | 4 |
| C003 | sns > email | 0 | 2 |
| C004 | direct | 1 | 1 |
| C005 | seo > display > display > sns | 0 | 4 |
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_channel | to_channel | transition_count | transition_prob |
|---|---|---|---|
| Start | seo | 8,412 | 0.4820 |
| Start | sns | 4,218 | 0.2416 |
| Start | direct | 2,841 | 0.1627 |
| Start | 1,284 | 0.0735 | |
| Start | display | 701 | 0.0402 |
| seo | sns | 3,412 | 0.3124 |
| seo | 2,841 | 0.2601 | |
| seo | Conversion | 1,218 | 0.1115 |
| seo | display | 1,084 | 0.0992 |
| seo | Null | 2,384 | 0.2183 |
| Conversion | 2,184 | 0.4812 | |
| Null | 2,351 | 0.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_journeys | total_conversions | overall_conversion_rate |
|---|---|---|
| 17,456 | 4,812 | 0.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_name | overall_cr | removal_cr | removal_effect | attribution_pct |
|---|---|---|---|---|
| 0.2756 | 0.1891 | 0.0865 | 34.2% | |
| seo | 0.2756 | 0.2084 | 0.0672 | 26.6% |
| sns | 0.2756 | 0.2241 | 0.0515 | 20.4% |
| direct | 0.2756 | 0.2491 | 0.0265 | 10.5% |
| display | 0.2756 | 0.2554 | 0.0202 | 8.0% |
| 合計 | — | — | 0.2519 | 100% |
アトリビューションモデルの比較
同じデータでラストタッチ・ファーストタッチ・均等分配・マルコフを並べると、モデルによる差が浮き彫りになる。
-- 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 の結果を追記)
| model | channel | attribution_pct |
|---|---|---|
| ラストタッチ | 58.2% | |
| ラストタッチ | direct | 24.1% |
| ラストタッチ | seo | 12.4% |
| ファーストタッチ | seo | 48.2% |
| ファーストタッチ | sns | 24.2% |
| ファーストタッチ | direct | 16.3% |
| マルコフ削除効果 | 34.2% | |
| マルコフ削除効果 | seo | 26.6% |
| マルコフ削除効果 | sns | 20.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 実装をまとめる。
- 顧客の接触履歴を
ARRAY_JOIN+ARRAY_AGGで経路文字列に変換する LEADを使って「次の状態」を付与し、Start→ チャネル →Conversion/Nullの遷移行列を作る- 遷移確率 = 遷移回数 ÷ そのチャネルからの合計遷移数 として計算する
- 各チャネルを除外したとき(
path LIKE '%channel%'の転換をゼロにしたとき)の転換率を計算する - 全体転換率との差分(削除効果)を正規化してアトリビューションシェアとする
「最後に触ったチャネルが一番偉い」というラストタッチの偏見から脱して、SEO・SNS・メルマガそれぞれの本当の役割と貢献度を定量化することが、広告予算配分の精度を高める。このクエリは、予算の意思決定を「なんとなく」から「データに基づく根拠」へと変える道具だ。