本文へ移動
顧客分析SQL 更新日: 2026年5月2日 約25分で読めます

生存時間分析をSQLで近似する方法|顧客が離脱するまでの期間を測る

サバイバル分析を用いることで、顧客離脱の時期とパターンを把握し、継続率だけでは見えない顧客像を明確化する。Kaplan-Meier推定量を利用し、打ち切りデータを正しく処理することで、顧客の生存率やハザード率を算出し、リテンション戦略に活用できる。


「何人が残っているか」より「いつ離脱するか」を知る

コホート分析では「M+1に何%が購買したか」という月次継続率を追った。これは強力な手法だが、1つの情報を失っている。

「離脱したのは何ヶ月目か」という時間の情報だ。

たとえば「6ヶ月後の継続率20%」という数字があるとき、残り80%は均等に1〜6ヶ月目に離脱したのか、それとも大半が1ヶ月目に離脱してその後は緩やかなのか。この2つは全く異なる顧客像を示すが、継続率の数字だけでは区別できない。

サバイバル分析(生存時間分析)は医学・工学で発展した統計手法で、「イベント(死亡・故障・離脱)が起きるまでの時間」の分布を分析する。通販に応用すると「顧客が離脱するまでの時間の分布」を正確に把握できる。

代表的な手法が Kaplan-Meier 推定量だ。ノンパラメトリック(分布を仮定しない)なため、どんな形の離脱パターンにも対応できる。今回はこれをSQLで実装する。


サバイバル分析の2つのキーコンセプト

生存関数 S(t)

S(t) = P(T > t) = 時点 t より後まで「生存」している確率

通販では「入会からt日後もまだアクティブである確率」だ。S(0) = 1.0(全員スタート時にアクティブ)で、時間とともに単調減少する。

打ち切り(Censoring)

サバイバル分析の最大の特徴は「打ち切り」を扱えることだ。

分析の基準日時点でまだ離脱していない顧客は「まだ生存している」ことは分かるが、いつ離脱するかはまだ不明だ。これを「右打ち切り」という。この顧客を「分析終了時点で離脱した」と誤分類すると生存率を過小評価し、「分析から除外する」とバイアスが生じる。打ち切りデータを正しく扱うのが Kaplan-Meier の強みだ。


「離脱」の定義

通販で「離脱」をどう定義するかは業務要件次第だ。今回は次の定義を使う。

「最終購買から180日(約6ヶ月)以上、購買が確認されない顧客を離脱とみなす」

基準日時点でまだ180日経っていない顧客は「打ち切り」として扱う。


使用するテーブル

-- orders テーブル
-- order_id      : 注文ID
-- customer_id   : 顧客ID
-- order_date    : 注文日
-- status        : 'completed' / 'cancelled'

STEP 1 ― 顧客ごとの「生存時間」と「イベント発生フラグ」を計算する

Kaplan-Meier に必要な入力は各顧客について2つだ。

  • duration:観察した期間(登録から離脱または打ち切りまでの日数)
  • event:その期間中に離脱イベントが発生したか(1=発生、0=打ち切り)
-- STEP1: 生存時間とイベントフラグの計算

WITH customer_orders AS (
    SELECT
        customer_id,
        MIN(order_date)  AS first_order_date,
        MAX(order_date)  AS last_order_date,
        COUNT(order_id)  AS purchase_count
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
survival_data AS (
    SELECT
        customer_id,
        first_order_date,
        last_order_date,
        purchase_count,

        -- 基準日
        DATE '2024-12-31'  AS reference_date,

        -- 最終購買からの経過日数
        DATE_DIFF('day', last_order_date, DATE '2024-12-31')
            AS days_since_last_order,

        -- イベント(離脱)の発生判定
        -- 180日以上沈黙 = 離脱イベント発生
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180
            THEN 1 ELSE 0
        END  AS event_occurred,

        -- 生存時間(duration)
        -- イベント発生の場合:初回購買から離脱判定日まで
        -- 打ち切りの場合:初回購買から基準日まで
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180
            -- 離脱した場合、「最終購買日 + 180日」が離脱判定日
            THEN DATE_DIFF('day', first_order_date,
                     DATE_ADD('day', 180, last_order_date))
            -- 打ち切りの場合、基準日までの全期間
            ELSE DATE_DIFF('day', first_order_date, DATE '2024-12-31')
        END  AS duration_days
    FROM customer_orders
)
SELECT
    customer_id,
    first_order_date,
    last_order_date,
    purchase_count,
    days_since_last_order,
    event_occurred,
    duration_days,
    CASE WHEN event_occurred = 1 THEN '離脱' ELSE '打ち切り(継続中)' END  AS status_label
FROM survival_data
ORDER BY duration_days ASC;

出力イメージ

customer_idfirst_order_datelast_order_dateevent_occurredduration_daysstatus_label
C0892024-11-152024-11-15046打ち切り(継続中)
C2342024-09-012024-09-010121打ち切り(継続中)
C1122022-03-102022-08-011331離脱
C0012022-08-052024-10-200878打ち切り(継続中)

STEP 2 ― Kaplan-Meier 推定量をSQLで計算する

Kaplan-Meier の計算ステップは次の通りだ。

各時点 t ごとに:
  n_t = その時点で「生存中」だった顧客数(リスク集合)
  d_t = その時点で「離脱」した顧客数
  S(t) = S(t-1) × (1 - d_t / n_t)

「生存中」とは「その時点までまだ離脱していない、かつ打ち切られていない顧客」だ。

-- STEP2: Kaplan-Meier 推定量の計算

WITH customer_orders AS (
    SELECT
        customer_id,
        MIN(order_date) AS first_order_date,
        MAX(order_date) AS last_order_date
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
survival_data AS (
    SELECT
        customer_id,
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180 THEN 1
            ELSE 0
        END  AS event_occurred,
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180
            THEN DATE_DIFF('day', first_order_date, DATE_ADD('day', 180, last_order_date))
            ELSE DATE_DIFF('day', first_order_date, DATE '2024-12-31')
        END  AS duration_days
    FROM customer_orders
),
-- 時点ごとのイベント数・打ち切り数を集計
-- 30日単位のビンに丸める(日次だと行数が多すぎるため)
event_table AS (
    SELECT
        -- 30日単位のビン
        FLOOR(duration_days / 30) * 30  AS time_bin,
        SUM(event_occurred)              AS events,       -- その期間の離脱数
        COUNT(*) - SUM(event_occurred)   AS censored      -- その期間の打ち切り数
    FROM survival_data
    GROUP BY FLOOR(duration_days / 30) * 30
),
-- リスク集合の計算(各時点での「生存中」の人数)
risk_set AS (
    SELECT
        time_bin,
        events,
        censored,
        -- リスク集合 = 全体 - その時点より前に離脱・打ち切りになった人数
        SUM(events + censored) OVER (
            ORDER BY time_bin DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )  AS at_risk
    FROM event_table
),
-- Kaplan-Meier 推定量の計算
km_estimate AS (
    SELECT
        time_bin,
        at_risk,
        events,
        censored,
        -- 各時点の生存確率の積(KM推定の核心)
        EXP(SUM(LN(CASE WHEN at_risk > 0
                   THEN 1.0 - events * 1.0 / at_risk
                   ELSE 1.0 END))
            OVER (ORDER BY time_bin ASC
                  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
        )  AS survival_prob,
        -- Greenwood の公式による95%信頼区間の近似
        -- Var(log S(t)) ≈ Σ d_i / [n_i(n_i - d_i)]
        SUM(
            CASE WHEN at_risk > 1 AND events > 0
                 THEN events * 1.0 / (at_risk * (at_risk - events))
                 ELSE 0 END
        ) OVER (ORDER BY time_bin ASC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)  AS greenwood_var
    FROM risk_set
)
SELECT
    time_bin                                                AS days_since_first_order,
    at_risk,
    events                                                  AS churn_events,
    censored,
    ROUND(survival_prob, 4)                                 AS survival_rate,
    ROUND((1 - survival_prob) * 100, 1)                    AS churn_rate_pct,

    -- 95%信頼区間(Greenwood公式 + 対数変換)
    ROUND(GREATEST(0, survival_prob * EXP(-1.96 * SQRT(greenwood_var))), 4)  AS ci_lower,
    ROUND(LEAST(1,   survival_prob * EXP( 1.96 * SQRT(greenwood_var))), 4)   AS ci_upper
FROM km_estimate
ORDER BY time_bin;

出力イメージ

days_since_first_orderat_riskchurn_eventssurvival_ratechurn_rate_pctci_lowerci_upper
014,21801.00000.01.00001.0000
3014,2188920.93736.30.93180.9428
6013,3261,2410.849215.10.84210.8563
9012,0859840.780122.00.77220.7880
1809,4121,1840.628437.20.61920.6376
3655,8418920.501249.90.49120.5112
5403,2845120.419858.00.40910.4305
7301,8412840.368463.20.35680.3800

中央生存時間(S(t) = 0.5 になる時点)は365日前後だ。 つまり「初回購買から1年で約半数の顧客が離脱する」という実態が定量化された。


STEP 3 ― コホート別に生存曲線を比較する

「どの流入チャネルからの顧客が長く残るか」「どの初回購買カテゴリが定着率が高いか」をチャネル別に比較する。

-- STEP3: チャネル別 Kaplan-Meier 比較

WITH customer_base AS (
    SELECT
        o.customer_id,
        c.channel,
        MIN(o.order_date)  AS first_order_date,
        MAX(o.order_date)  AS last_order_date
    FROM orders     o
    JOIN customers  c  ON o.customer_id = c.customer_id
    WHERE o.status = 'completed'
    GROUP BY o.customer_id, c.channel
),
survival_data AS (
    SELECT
        customer_id,
        channel,
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180 THEN 1
            ELSE 0
        END  AS event_occurred,
        CASE
            WHEN DATE_DIFF('day', last_order_date, DATE '2024-12-31') >= 180
            THEN DATE_DIFF('day', first_order_date, DATE_ADD('day', 180, last_order_date))
            ELSE DATE_DIFF('day', first_order_date, DATE '2024-12-31')
        END  AS duration_days
    FROM customer_base
),
event_table AS (
    SELECT
        channel,
        FLOOR(duration_days / 90) * 90  AS time_bin,  -- 90日単位
        SUM(event_occurred)              AS events,
        COUNT(*) - SUM(event_occurred)   AS censored
    FROM survival_data
    GROUP BY channel, FLOOR(duration_days / 90) * 90
),
risk_set AS (
    SELECT
        channel,
        time_bin,
        events,
        SUM(events + censored) OVER (
            PARTITION BY channel
            ORDER BY time_bin DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )  AS at_risk
    FROM event_table
)
SELECT
    channel,
    time_bin  AS days,
    at_risk,
    events,
    ROUND(
        EXP(SUM(LN(CASE WHEN at_risk > 0
                   THEN 1.0 - events * 1.0 / at_risk
                   ELSE 1.0 END))
            OVER (PARTITION BY channel
                  ORDER BY time_bin ASC
                  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW))
    , 4)  AS survival_rate
FROM risk_set
ORDER BY channel, time_bin;

出力イメージ(180日・365日の生存率)

channeldays=180 survivaldays=365 survival中央生存時間の目安
referral0.74120.5892390日以上
organic0.68410.5124370日前後
paid0.59210.4012290日前後
social0.51240.3412240日前後

紹介(referral)経由の顧客は180日後も74%が生存しており、有料広告(paid)の59%・SNS(social)の51%と比べて明らかに離脱が遅い。この差は顧客獲得コスト(CPA)の評価に直結する。


STEP 4 ― ハザード率(離脱の瞬間的な確率)を計算する

生存曲線の「傾き」にあたるのがハザード率だ。「この時点で生存している顧客が、次の期間に離脱する確率」を示す。

-- STEP4: ハザード率の計算(どの時点で最も離脱リスクが高いか)

WITH km_data AS (
    -- STEP2の集計済みテーブルを使う(ここでは簡略化)
    SELECT
        time_bin,
        at_risk,
        events,
        ROUND(
            EXP(SUM(LN(CASE WHEN at_risk > 0
                       THEN 1.0 - events * 1.0 / at_risk
                       ELSE 1.0 END))
                OVER (ORDER BY time_bin ASC
                      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW))
        , 4)  AS survival_rate
    FROM (
        -- STEP2のrisk_set相当の集計
        SELECT
            FLOOR(duration_days / 30) * 30  AS time_bin,
            SUM(events + censored) OVER (ORDER BY FLOOR(duration_days / 30) * 30 DESC
                                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS at_risk,
            SUM(event_occurred)  AS events,
            COUNT(*) - SUM(event_occurred)  AS censored
        FROM (
            SELECT
                CASE WHEN DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') >= 180 THEN 1 ELSE 0 END AS event_occurred,
                CASE WHEN DATE_DIFF('day', MAX(order_date), DATE '2024-12-31') >= 180
                     THEN DATE_DIFF('day', MIN(order_date), DATE_ADD('day', 180, MAX(order_date)))
                     ELSE DATE_DIFF('day', MIN(order_date), DATE '2024-12-31') END AS duration_days
            FROM orders WHERE status='completed' GROUP BY customer_id
        )
        GROUP BY FLOOR(duration_days / 30) * 30
    )
)
SELECT
    time_bin  AS days,
    at_risk,
    events,
    survival_rate,
    -- ハザード率 = 各時点での離脱数 / リスク集合
    ROUND(events * 1.0 / NULLIF(at_risk, 0), 4)  AS hazard_rate,
    -- 前の期間との比較
    LAG(events * 1.0 / NULLIF(at_risk, 0)) OVER (ORDER BY time_bin)  AS prev_hazard
FROM km_data
ORDER BY time_bin;

出力イメージ

dayshazard_rate解釈
0〜300.0628最初の1ヶ月:初期離脱が多い
30〜600.09311〜2ヶ月:ハザードがピーク
60〜900.08152〜3ヶ月:やや落ち着く
90〜1800.03123〜6ヶ月:安定化
180〜3650.01986ヶ月〜1年:ベースライン
365以上0.01241年超:長期顧客は安定

ハザード率が最も高いのは「1〜2ヶ月目」だ。2回目購買を促すフォローアップ施策をこの時期に集中させることが、最大のリテンション効果をもたらす。


実務での運用ヒント

① 「離脱」の定義は慎重に決める

180日という閾値は一例だ。商材の購買サイクルが短い(週次・月次)なら30〜60日、長い(季節商材・年一回)なら365日を使うケースもある。No.2(購買間隔の分布分析)でその顧客基盤の標準的な購買間隔を先に把握したうえで、p90(90パーセンタイル)の間隔を「離脱の閾値」として使う設計が理論的に整合している。

② 30日単位のビンへの丸め

厳密な KM 推定は「イベントが発生した正確な日付」ごとに計算するが、それでは出力が数百行になり解釈が難しい。月次(30日)や四半期(90日)でビン化することで可読性を保つ。精度よりも「形の把握」を優先する実務では十分だ。

③ ログランク検定でコホートの差を統計的に検証する

STEP3でチャネル別の生存曲線を比較したが、「この差は統計的に有意か」をSQLだけで検証するのは難しい。Python の lifelines ライブラリの logrank_test 関数を使うと、2つのコホートの生存曲線の差が偶然かどうかを検定できる。SQL で集計したデータを Python に渡して検定するのが現実的なフローだ。


まとめ

サバイバル分析の SQL 実装をまとめる。

  1. 顧客ごとに「生存時間(duration_days)」と「イベントフラグ(event_occurred)」を計算する
  2. 打ち切り(基準日時点でまだ離脱していない顧客)を正しく区別して扱う
  3. 時点ごとの「リスク集合(at_risk)」を SUM() OVER (ORDER BY time DESC) の累積和で計算する
  4. EXP(SUM(LN(...)) OVER (...)) で Kaplan-Meier 推定量(積の累積)を計算する
  5. Greenwood 公式で 95% 信頼区間を追加し、結果の信頼性を示す

コホート分析が「どの月に何%が購買したか」を追うのに対し、サバイバル分析は「どの時点で何%が離脱したか、その傾きはどうか」を追う。ハザード率の山が「初回購買後1〜2ヶ月目」にあると分かれば、施策を投入すべき時期が自ずと決まる。これがデータドリブンなリテンション戦略の出発点だ。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

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

続きを読む