「そろそろ買う頃」を数字で定義できるか
通販の休眠顧客対策でよくある失敗がある。
「最終購買から90日経ったら休眠」という定義を全顧客に一律で適用してしまうことだ。
しかし消耗品を毎月買う顧客と、季節ごとに年2回しか買わない顧客を同じ「90日ルール」で管理するのは正確ではない。前者にとって90日は完全な離脱だが、後者にとって90日はごく普通の間隔だ。
顧客ごとの「正常な購買間隔」を知ることができれば、「この顧客にとっての異常な沈黙」を精確に検知できる。それがこの記事のテーマだ。
LAG 関数で前回購買日を取得し、購買間隔の平均・中央値・標準偏差・パーセンタイル分布をSQLで計算する。最終的に「次の購買予測日」と「現在の沈黙が異常かどうか」を判定するクエリを作る。
使用するテーブル
-- orders テーブル
-- order_id : 注文ID
-- customer_id : 顧客ID
-- order_date : 注文日(DATE型)
-- total_amount : 注文金額
-- status : 'completed' / 'cancelled'STEP 1 ― LAG で前回購買日を取得し購買間隔を計算する
まず各注文に「前回の購買日」と「前回からの経過日数」を付与する。
-- STEP1: 購買間隔の計算
WITH order_history AS (
SELECT
customer_id,
order_date,
-- 同じ顧客の1つ前の注文日を取得
LAG(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date ASC
) AS prev_order_date
FROM orders
WHERE status = 'completed'
),
purchase_intervals AS (
SELECT
customer_id,
order_date,
prev_order_date,
-- 前回購買からの日数(初回購買は NULL)
DATE_DIFF('day', prev_order_date, order_date) AS interval_days
FROM order_history
WHERE prev_order_date IS NOT NULL -- 初回購買(比較対象なし)を除外
)
SELECT *
FROM purchase_intervals
ORDER BY customer_id, order_date;出力イメージ(顧客 C001 の例)
| customer_id | order_date | prev_order_date | interval_days |
|---|---|---|---|
| C001 | 2024-02-12 | 2024-01-08 | 35 |
| C001 | 2024-03-05 | 2024-02-12 | 22 |
| C001 | 2024-04-18 | 2024-03-05 | 44 |
| C001 | 2024-05-09 | 2024-04-18 | 21 |
| C001 | 2024-08-01 | 2024-05-09 | 84 |
C001 の購買間隔は22〜84日とばらつきがある。84日の間隔は「5月に買ってから8月まで空いた」ものだ。これが「異常な沈黙」なのか「たまたまそうなった」のかは、この顧客の全体的な間隔の分布を見なければ判断できない。
STEP 2 ― 顧客ごとの購買間隔統計を計算する
購買間隔が揃ったので、顧客ごとに統計量を集計する。
-- STEP2: 顧客ごとの購買間隔統計
WITH order_history AS (
SELECT
customer_id,
order_date,
LAG(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date ASC
) AS prev_order_date
FROM orders
WHERE status = 'completed'
),
purchase_intervals AS (
SELECT
customer_id,
order_date,
DATE_DIFF('day', prev_order_date, order_date) AS interval_days
FROM order_history
WHERE prev_order_date IS NOT NULL
),
interval_stats AS (
SELECT
customer_id,
COUNT(*) AS purchase_count, -- 購買回数(間隔の数)
ROUND(AVG(interval_days), 1) AS avg_interval, -- 平均間隔
ROUND(STDDEV(interval_days), 1) AS stddev_interval, -- 標準偏差
MIN(interval_days) AS min_interval, -- 最短間隔
MAX(interval_days) AS max_interval, -- 最長間隔
-- 中央値(Presto では APPROX_PERCENTILE を使う)
APPROX_PERCENTILE(interval_days, 0.5) AS median_interval,
-- 75パーセンタイル(「少し長め」の間隔)
APPROX_PERCENTILE(interval_days, 0.75) AS p75_interval,
-- 90パーセンタイル(「かなり長め」の間隔)
APPROX_PERCENTILE(interval_days, 0.90) AS p90_interval
FROM purchase_intervals
GROUP BY customer_id
HAVING COUNT(*) >= 2 -- 間隔を計算できる最低回数(2回以上の購買)
)
SELECT *
FROM interval_stats
ORDER BY avg_interval ASC;出力イメージ
| customer_id | purchase_count | avg_interval | stddev_interval | median_interval | p75_interval | p90_interval |
|---|---|---|---|---|---|---|
| C004 | 18 | 18.2 | 4.1 | 18 | 21 | 24 |
| C001 | 5 | 41.2 | 23.8 | 35 | 44 | 79 |
| C012 | 4 | 62.4 | 18.2 | 60 | 76 | 84 |
| C003 | 3 | 124.0 | 35.6 | 118 | 152 | 161 |
C004 は平均18.2日でほぼ毎月コンスタントに購買している顧客(標準偏差も4.1と小さく安定)。C003 は平均124日で、年に2〜3回の購買パターンだ。この2人に同じ「90日経ったら休眠」を適用するのがいかに不適切かが数字で分かる。
STEP 3 ― 次の購買予測日と「異常な沈黙」を判定する
顧客ごとの間隔統計と「最終購買日」を組み合わせて、「次の購買予測日」と「現在の沈黙が統計的に異常かどうか」を判定する。
使う考え方はシンプルだ。
予測次回購買日 = 最終購買日 + 平均間隔
異常の閾値 = 最終購買日 + (平均間隔 + 標準偏差 × 2)
統計学的には「平均 + 標準偏差 × 2」を超えると、正規分布の約95%の範囲外になる。
これを「異常な沈黙」の目安とする。
-- STEP3: 次回購買予測と異常沈黙の判定(完成版)
WITH order_history AS (
SELECT
customer_id,
order_date,
LAG(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date ASC
) AS prev_order_date
FROM orders
WHERE status = 'completed'
),
purchase_intervals AS (
SELECT
customer_id,
order_date,
DATE_DIFF('day', prev_order_date, order_date) AS interval_days
FROM order_history
WHERE prev_order_date IS NOT NULL
),
last_purchase AS (
-- 最終購買日と最終購買からの経過日数
SELECT
customer_id,
MAX(order_date) AS last_order_date,
DATE_DIFF('day', MAX(order_date), CURRENT_DATE) AS days_since_last_order
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
),
interval_stats AS (
SELECT
customer_id,
COUNT(*) AS purchase_count,
ROUND(AVG(interval_days), 1) AS avg_interval,
ROUND(STDDEV(interval_days), 1) AS stddev_interval,
APPROX_PERCENTILE(interval_days, 0.5) AS median_interval,
APPROX_PERCENTILE(interval_days, 0.90) AS p90_interval
FROM purchase_intervals
GROUP BY customer_id
HAVING COUNT(*) >= 2
)
SELECT
lp.customer_id,
lp.last_order_date,
lp.days_since_last_order,
ist.avg_interval,
ist.stddev_interval,
ist.median_interval,
-- 次回購買予測日(最終購買日 + 平均間隔)
DATE_ADD('day',
CAST(ist.avg_interval AS INTEGER),
lp.last_order_date
) AS predicted_next_order_date,
-- 「そろそろ購買タイミング」フラグ
-- (現在日が予測日の ±7日以内)
CASE
WHEN ABS(
DATE_DIFF('day',
DATE_ADD('day', CAST(ist.avg_interval AS INTEGER), lp.last_order_date),
CURRENT_DATE
)
) <= 7
THEN 1 ELSE 0
END AS in_predicted_window,
-- 異常な沈黙の閾値(平均 + 標準偏差 × 2)
CAST(ist.avg_interval + ist.stddev_interval * 2 AS INTEGER)
AS silence_threshold_days,
-- 異常フラグ(閾値を超えたら要注意)
CASE
WHEN lp.days_since_last_order
> ist.avg_interval + ist.stddev_interval * 2
THEN '⚠ 異常な沈黙'
WHEN lp.days_since_last_order > ist.avg_interval
THEN '購買タイミング超過'
ELSE '正常範囲'
END AS silence_status
FROM last_purchase lp
JOIN interval_stats ist ON lp.customer_id = ist.customer_id
ORDER BY
CASE
WHEN lp.days_since_last_order > ist.avg_interval + ist.stddev_interval * 2
THEN 1
WHEN lp.days_since_last_order > ist.avg_interval
THEN 2
ELSE 3
END,
lp.days_since_last_order DESC;完成した出力イメージ
| customer_id | last_order_date | days_since_last | avg_interval | silence_threshold | in_predicted_window | silence_status |
|---|---|---|---|---|---|---|
| C001 | 2024-10-12 | 85 | 41.2 | 89 | 0 | ⚠ 異常な沈黙 |
| C007 | 2024-11-01 | 65 | 62.4 | 99 | 1 | 購買タイミング超過 |
| C004 | 2024-12-08 | 19 | 18.2 | 26 | 1 | 正常範囲 |
| C003 | 2024-09-15 | 82 | 124.0 | 195 | 0 | 正常範囲 |
C001 は85日間沈黙しており、その顧客の閾値89日にあと4日で達する。今すぐウィンバック施策を打つべき顧客だ。C003 は82日間沈黙しているが、その顧客の平均間隔が124日なので「正常範囲」と正しく判定される。C007 は予測購買ウィンドウ内(±7日)にいるため「今まさに購買しそうな顧客」だ。
STEP 4 ― 購買間隔のヒストグラムを全体で出す
個人ではなく全顧客の購買間隔分布を可視化するクエリも作っておこう。間隔を帯(バケット)に分けて件数を出すことで、「全体として顧客はどんなサイクルで買っているか」が見える。
-- STEP4: 購買間隔のヒストグラム(全顧客)
WITH order_history AS (
SELECT
customer_id,
order_date,
LAG(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date ASC
) AS prev_order_date
FROM orders
WHERE status = 'completed'
),
purchase_intervals AS (
SELECT
customer_id,
DATE_DIFF('day', prev_order_date, order_date) AS interval_days
FROM order_history
WHERE prev_order_date IS NOT NULL
),
bucketed AS (
SELECT
CASE
WHEN interval_days <= 7 THEN '01_〜7日'
WHEN interval_days <= 14 THEN '02_8〜14日'
WHEN interval_days <= 30 THEN '03_15〜30日'
WHEN interval_days <= 60 THEN '04_31〜60日'
WHEN interval_days <= 90 THEN '05_61〜90日'
WHEN interval_days <= 180 THEN '06_91〜180日'
WHEN interval_days <= 365 THEN '07_181〜365日'
ELSE '08_365日超'
END AS interval_bucket,
interval_days
FROM purchase_intervals
)
SELECT
interval_bucket,
COUNT(*) AS interval_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS pct,
ROUND(AVG(interval_days), 1) AS avg_days_in_bucket
FROM bucketed
GROUP BY interval_bucket
ORDER BY interval_bucket;
出力イメージ
| interval_bucket | interval_count | pct | avg_days_in_bucket |
|---|---|---|---|
| 01_〜7日 | 1,240 | 8.4 | 4.2 |
| 02_8〜14日 | 1,890 | 12.8 | 11.3 |
| 03_15〜30日 | 3,412 | 23.1 | 22.8 |
| 04_31〜60日 | 4,218 | 28.5 | 44.1 |
| 05_61〜90日 | 2,184 | 14.8 | 74.2 |
| 06_91〜180日 | 1,442 | 9.8 | 128.4 |
| 07_181〜365日 | 384 | 2.6 | 241.2 |
| 08_365日超 | 0 | 0.0 | NULL |
全体の52%(01〜04)が60日以内に次の購買をしている。
一方で10%(91〜180日)の顧客は平均128日サイクルだ。「90日ルール」を一律に適用すると、この10%のグループを正常購買中にもかかわらず「休眠」と誤判定する可能性がある。
実務での運用ヒント
① 購買回数が少ない顧客は統計が不安定
2〜3回しか購買歴がない顧客の平均間隔は、1回の外れ値に大きく引っ張られる。HAVING COUNT(*) >= 5(5回以上)など、購買回数の最低基準を上げると精度が上がる。回数が少ない顧客には個別統計ではなく「同じカテゴリ購買者の中央値」を代わりに使う設計も有効だ。
② STDDEV が NULL になるケース
購買間隔が1件だけ(購買が2回のみで間隔が1つしかない)の場合、標準偏差は計算できず NULL になる。閾値計算で avg_interval + stddev_interval * 2 が NULL になるのを防ぐため、COALESCE(stddev_interval, avg_interval * 0.5) のように「標準偏差が不明なら平均の半分を代わりに使う」フォールバックを入れておくと安全だ。
③ 購買間隔の「正規分布」仮定の限界
「平均 + 標準偏差 × 2」は正規分布を前提にした閾値だ。
実際の購買間隔は右に歪んだ分布(一部の顧客が極端に長い間隔を持つ)になりやすい。より精度を上げたい場合は p90_interval(90パーセンタイル)を閾値として使う方が実態に近いことが多い。
まとめ
今回のクエリの流れを振り返る。
LAG(order_date)で前回購買日を取得し、DATE_DIFFで購買間隔(日数)を計算するAVG/STDDEV/APPROX_PERCENTILEで顧客ごとの間隔統計を算出する最終購買日 + 平均間隔で次回購買予測日を計算し、予測ウィンドウ内かどうかをフラグ化する平均 + 標準偏差 × 2を異常沈黙の閾値として、顧客ごとに個別の休眠判定を行う- 全顧客のヒストグラムで全体の購買サイクル分布を把握する
「90日で一律休眠」から「この顧客にとっての異常な沈黙」への転換が、このクエリで実現できる。顧客ごとの閾値でウィンバック対象を選ぶことで、施策の精度と費用対効果が大きく改善する。