本文へスキップ
martechfarmDATA & TECHNOLOGY DOCS

すべての記事のタイトル・本文を検索

すべての記事

127 件

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

F2転換を改善する方法|2回目割引に頼らないCRM施策設計

再帰CTEの使い方|SQLでカレンダーと階層構造を生成する方法

PIVOT・UNPIVOTをSQLで実装する方法|CASE WHENで縦横変換する

勇者は最初から勇者ではない|未経験からキャリアを育てる転職論

SQLウィンドウ関数の使い方|分析クエリで必須のOVER句を理解する

ファーストパーティデータとは何か|誤解されやすい意味とCDP活用の本質

Treasure Data導入で失敗する会社の共通点|CDPを使いこなせない5つの理由

ROLLUP・CUBEで小計と総計をSQL集計する方法

EXCEPT・INTERSECTで顧客セグメント差分を抽出するSQL

パーソナライゼーションが気持ち悪い理由|顧客体験を壊さないデータ活用

中小企業がデータ活用で大企業に勝つ方法|小さな会社のCDP戦略

データエンジニアは10年後も必要か|AI時代に残る仕事と変わる役割

Treasure Dataが選ばれる理由|CDP導入で評価される強みと実務メリット

価格改定履歴をSQLで扱う方法|注文時点の正しい単価を取得するクエリ

マーケターがSQLを学ぶべき理由|2030年に必要なデータ分析スキル

ポイント残高をSQLで再計算する方法|トランザクションログから正しい残高を出す

SCDをSQLで実装する方法|履歴を保持するディメンション設計

SQLを学ぶべき理由|マーケターとデータ人材に必要な共通言語

最新レコードをSQLで取得する方法|ROW_NUMBERとDELETE INSERTの使い分け

Digdagのparallel設定を最適化する方法|夜間バッチを高速化する実務設計

セッションIDなしのアクセスログをSQLでセッション化する方法

曜日・時間帯別の売上をSQLで集計する方法|注文集中時間を可視化する

F2転換をSQLで分析する方法|初回から2回目購買までのカテゴリ経路を見る

購買間隔をSQLで分析する方法|次回購入タイミングを予測するクエリ

本文
記事一覧へ ↑

購買間隔をSQLで分析する方法|次回購入タイミングを予測するクエリ


「そろそろ買う頃」を数字で定義できるか

通販の休眠顧客対策でよくある失敗がある。
「最終購買から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_idorder_dateprev_order_dateinterval_days
C0012024-02-122024-01-0835
C0012024-03-052024-02-1222
C0012024-04-182024-03-0544
C0012024-05-092024-04-1821
C0012024-08-012024-05-0984

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_idpurchase_countavg_intervalstddev_intervalmedian_intervalp75_intervalp90_interval
C0041818.24.1182124
C001541.223.8354479
C012462.418.2607684
C0033124.035.6118152161

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_idlast_order_datedays_since_lastavg_intervalsilence_thresholdin_predicted_windowsilence_status
C0012024-10-128541.2890⚠ 異常な沈黙
C0072024-11-016562.4991購買タイミング超過
C0042024-12-081918.2261正常範囲
C0032024-09-1582124.01950正常範囲

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_bucketinterval_countpctavg_days_in_bucket
01_〜7日1,2408.44.2
02_8〜14日1,89012.811.3
03_15〜30日3,41223.122.8
04_31〜60日4,21828.544.1
05_61〜90日2,18414.874.2
06_91〜180日1,4429.8128.4
07_181〜365日3842.6241.2
08_365日超00.0NULL

全体の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パーセンタイル)を閾値として使う方が実態に近いことが多い。


まとめ

今回のクエリの流れを振り返る。

  1. LAG(order_date) で前回購買日を取得し、DATE_DIFF で購買間隔(日数)を計算する
  2. AVG / STDDEV / APPROX_PERCENTILE で顧客ごとの間隔統計を算出する
  3. 最終購買日 + 平均間隔 で次回購買予測日を計算し、予測ウィンドウ内かどうかをフラグ化する
  4. 平均 + 標準偏差 × 2 を異常沈黙の閾値として、顧客ごとに個別の休眠判定を行う
  5. 全顧客のヒストグラムで全体の購買サイクル分布を把握する

「90日で一律休眠」から「この顧客にとっての異常な沈黙」への転換が、このクエリで実現できる。顧客ごとの閾値でウィンバック対象を選ぶことで、施策の精度と費用対効果が大きく改善する。


MarTech Farmをもっと見る

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

続きを読む