※ 本記事は 2022 年にNoteに書いた記事をリライトして移植したものです。
プロダクトの分析においては、ユーザーを任意にセグメンテーションして、より好ましいセグメントに遷移してもらうように施策を考える、ということを行うことがあります。例えば RFM 分析では Recency, Frequency, Monetary の 3 つの尺度でユーザーを分類して、現状を把握し、優良グループを増やすなどの施策を考えます。
ただ、状態の「分け方」に比べて、状態の「間の動き」を数字で扱う方法は、あまり語られない気がします。今月のユーザー構成は集計すれば分かる。では、いまいるユーザーは、何も手を打たなければ3ヶ月後にどうなっているのか。解約に至るまでには、平均で何ヶ月あるのか。この記事では、マルコフ連鎖という道具を使って、こうした問いに月次の遷移の集計だけから答える方法を紹介します。
マルコフ連鎖の基本
例えば状態が A と B の2つだけあって、ユーザーは毎月どちらかの状態にいるとします。A にいる人は、翌月 30% の確率で A に留まり、70% で B へ移る。B にいる人は 60% で A へ移り、40% で B に留まる。この移りやすさを1つの表にまとめたものが推移確率行列です。
行が「いまの状態」、列が「翌月の状態」に対応していて、各行の合計は必ず 1 になります。
この行列が便利なのは、掛け算がそのまま「時間を進める」操作になるところです。いま全体の 100% が A にいるとして、状態の分布を横ベクトル で表すと、翌月の分布は に を掛けた になります。さらにその翌月の分布は、この結果にもう一度 を掛ければ出てきます。つまり ヶ月後の分布は、 を 回掛けた行列 から読み取れます。
ただし、 をこのように読むには、2つの仮定を置いています。1つは、推移確率が時間によらず一定であること。もう1つは、翌月の状態が「いまの状態」だけで決まり、それ以前の履歴には依存しないことです。この仮定のもとでは、1年間使い込んだ末に休眠した人も、登録直後に休眠した人も、同じ「休眠」として扱われます。かなり大胆な割り切りですが、この割り切りが計算の見通しの良さと引き換えになっています。
ユーザーの状態遷移のモデル化
では、ユーザーの行動に当てはめてみます。例えば月次のアクティビティをもとに、状態を3つ定義します。
- 定着ユーザー: 前月も今月もアクティビティがある
- 休眠ユーザー: 今月のアクティビティがない
- 復帰ユーザー: 前月は休眠で、今月アクティビティがある
前月と今月のアクティビティの有無で場合分けしているだけなので、どのユーザーもちょうど1つの状態に入ります。今月アクティブでない人はまとめて休眠。今月アクティブな人は、前月しだいで定着と復帰に分かれます。
なお、新規ユーザーはこのモデルの外に置き、いま在籍しているユーザーだけを閉じたまま追いかけます。登録直後の月は「前月なし・今月あり」なので、この定義のままだと新規が復帰に混ざってしまうためです。実際に集計するときは、新規を別の状態に分けるか登録月を数えない形にして、サービス全体の構成を知りたい場合は、そのうえで毎月の新規流入を足し込むことになります。
そのうえで、遷移確率を次のように想定します。定着ユーザーは 40% で翌月も定着し、60% で休眠する。休眠ユーザーは 90% で休眠を続け、10% で復帰する。復帰ユーザーは 30% でそのまま定着し、70% で再び休眠する。実データでこの確率を得るには、過去データで月ごとの状態を判定して、隣り合う月の遷移を集計すれば足ります。
図には、矢印の引かれていない組み合わせが3つあります。定着から復帰、休眠から定着、復帰から復帰の3つです。これは確率が小さいという意味ではなく、状態の定義から起こりえない遷移です。前月が休眠だった人が今月アクティブなら、それは定着ではなく復帰。今月アクティブな人が翌月もアクティブなら、それは復帰ではなく定着。定義をそのまま辿ると、この3つは通れません。
推移確率行列にして、2ヶ月後・3ヶ月後を計算してみます(行・列とも定着、休眠、復帰の順)。
import numpy as np
np.set_printoptions(precision=4, suppress=True)
P = np.array([
[0.4, 0.6, 0.0], # 定着 →
[0.0, 0.9, 0.1], # 休眠 →
[0.3, 0.7, 0.0], # 復帰 →
])
print(np.linalg.matrix_power(P, 2))
# [[0.16 0.78 0.06]
# [0.03 0.88 0.09]
# [0.12 0.81 0.07]]
print(np.linalg.matrix_power(P, 3))
# [[0.082 0.84 0.078]
# [0.039 0.873 0.088]
# [0.069 0.85 0.081]]
の 1 行 1 列目を見ると、いま定着しているユーザーが 2 ヶ月後も定着している確率は 16%。3 ヶ月後には 8.2% まで下がります。
この行列は、施策の効果を見積もる叩き台にもなります。たとえばリテンション施策で「定着 → 定着」の確率を 0.4 から 0.5 に引き上げられたとすると、同じ計算で、2ヶ月後も定着している確率は 16% から 25% へ変わります(各行の合計は 1 のままにする必要があるので、ここでは増やした 0.1 の分「定着 → 休眠」を 0.6 から 0.5 に下げています。どのマスから持ってくるかは分析者の決めごとで、決め方しだいで答えも変わります)。遷移確率のどこを、どれだけ動かすと、少し先の構成がどう変わるか。施策を打つ前に、その感度を試算できるわけです。
もう1つ、 のべき乗を上げていくと面白いことが起きます。
print(np.linalg.matrix_power(P, 12))
# [[0.0435 0.8696 0.087 ]
# [0.0435 0.8696 0.087 ]
# [0.0435 0.8696 0.087 ]]
12ヶ月後には、どの行もほぼ同じ値に揃います。どの状態から出発しても、長期的には定着 4.3%、休眠 87.0%、復帰 8.7% という同じ構成に行き着くということです。この行き着く先は定常分布と呼ばれます。
解約という出口の追加
ここまでのモデルには、不自然な点が1つ残っています。ユーザーが永遠に、定着・休眠・復帰の間を回り続けることです。実際のプロダクトには、解約して二度と戻らないユーザーがいます。そこで「一度入ったら出られない状態」を導入します。こうした状態を吸収状態と呼び、吸収状態を持つマルコフ連鎖を吸収マルコフ連鎖と呼びます。
解約を入れると、さきほどの定常分布は見ても仕方のないものになります。時間さえ経てば全員が解約に行き着くので、行き先は 100% 解約で決まりだからです。そのかわり、問える内容が変わります。解約までにどれくらいの時間があるのか。その間、どの状態にどれだけ留まるのか。出口が複数あるなら、どちらへ行き着くのか。吸収マルコフ連鎖が答えるのは、これらの問いです。
解約を吸収状態として加えて、遷移確率を想定し直します。解約に回す確率は、いずれも休眠へ向かっていた分から取ることにします。定着ユーザーは 5%、復帰ユーザーは 8%、休眠ユーザーは 10% が翌月に解約する。アクティブでない時間が長い人ほど離れていきやすい、という置き方です。これ以外の遷移は、前の節から動かしていません。
行列では、吸収状態を先頭に並べます(行・列とも解約、定着、休眠、復帰の順)。
こう並べると、行列を2つの部品に分けられます。非吸収状態(定着・休眠・復帰)の間の推移だけを抜き出した 3×3 の部分を 、非吸収状態から吸収状態への推移を抜き出した部分を とします。
この を使うと、「解約までに各状態でどれだけ過ごすか」を計算できます。ポイントは、吸収状態が含まれているため、十分長い期間を考えると、いずれはすべてのユーザーが吸収状態へ遷移してしまうことです。 の 成分は「状態 から出発して、 ヶ月後にまだ解約せず状態 にいる確率」を表すので、これを未来の各期について と足し合わせていけば、解約までに各状態で過ごす月数の期待値が求まります。この無限和は、等比級数の公式 と同じ要領で という閉じた形にまとめられます。これをマルコフ連鎖の基本行列と呼び、 と表現します。
Q = np.array([
[0.4, 0.55, 0.0],
[0.0, 0.8, 0.1],
[0.3, 0.62, 0.0],
])
R = np.array([[0.05], [0.1], [0.08]])
N = np.linalg.inv(np.eye(3) - Q)
print(N)
# [[2.0814 8.2956 0.8296]
# [0.4525 9.0498 0.905 ]
# [0.905 8.0995 1.81 ]]
この の 成分は「状態 から出発したとき、解約までに状態 で過ごす月数の期待値」で、 の 1 行目は、いま定着しているユーザーの余生の内訳です。解約までに、定着のまま過ごすのが平均 2.08 ヶ月、休眠で過ごすのが 8.30 ヶ月、復帰で過ごすのが 0.83 ヶ月となります。行を合計すれば、解約までの平均期間になります(数え始めは今月なので、いずれも「あと何ヶ月」ではなく「今月を含めて何ヶ月」の数字です)。なお、コードに出てくる @ は、Python の行列積の演算子です(np.dot と同じ計算です)。
print(N @ np.ones(3))
# [11.2066 10.4072 10.8145]
定着ユーザーが解約に至るまでの平均は 11.2 ヶ月。ただしその内訳を見ると、定着として過ごすのは 2 ヶ月ほどで、残りの 8 ヶ月あまりは休眠のまま漂って解約に至る、という姿が見えます。平均 11 ヶ月と聞くと猶予があるように感じますが、施策が届きやすいアクティブな期間はずっと短いことがわかります。
吸収マルコフ連鎖にはもう1つ、 と の積で「各状態から出発して、最終的にどの吸収状態へ行き着くか」の確率を計算できるという性質があります。今回は出口が解約しかないので、計算するまでもなく、どこから出発しても行き着く先は 100% 解約です。実際に計算しても 1 が並びます。
print(N @ R)
# [[1.]
# [1.]
# [1.]]
この計算が生きてくるのは、出口が複数あるときです。
出口が2つある場合
出口を1つ加えてみます。有料プランへの移行が定着した状態(以下、有料化)です。有料化したユーザーはこのモデルの外へ出ていくと考えて、解約と並ぶ2つ目の吸収状態として扱います。
吸収状態にするというのは、その先を追うのをやめる、という宣言でもあります。現実には有料ユーザーも解約しますが、それはこのモデルの外側の話になります。このあと出てくる有料化の確率は「いつか有料化するか」であって、有料化したあとどれだけ続くかは含んでいません。
遷移確率は、有料化に回す分を「定着し続けていたはずの分」から取ることにします。定着ユーザーは 10%、復帰ユーザーは 5% が翌月に有料化するとして、その分定着へ向かう確率を下げます(定着の継続は 0.40 から 0.30 へ、復帰からの定着化は 0.30 から 0.25 へ)。休眠ユーザーは直接には有料化しないものとします。これ以外の確率は、前の節から動かしていません。
推移確率行列のブロック分割も同じ要領です。吸収状態が2つになった分、 が 3×2 に広がります。
# 行は定着, 休眠, 復帰。R の列は解約, 有料化の順
Q = np.array([
[0.30, 0.55, 0.0],
[0.0, 0.80, 0.1],
[0.25, 0.62, 0.0],
])
R = np.array([
[0.05, 0.10], # 定着 →
[0.10, 0.00], # 休眠 →
[0.08, 0.05], # 復帰 →
])
N = np.linalg.inv(np.eye(3) - Q)
print(N @ R)
# [[0.8002 0.1998]
# [0.9276 0.0724]
# [0.8552 0.1448]]
行が出発状態(定着・休眠・復帰)、列が行き着く先(解約・有料化)です。定着ユーザーは 20.0% が有料化に、80.0% が解約に行き着く。休眠ユーザーが有料化までたどり着くのは 7.2%、14 人に 1 人です。
面白いのは復帰ユーザーで、有料化率は 14.5% と、休眠のちょうど2倍になります。この 14.5% は、最初の1ヶ月で何が起きるかで場合分けすると分解できます。その月のうちに出口へ抜けてしまうか、いったん別の状態へ移って、そこから先の行き先に従うか。この場合分けを式にすると、吸収確率 が満たす次の関係になります。
右辺の が「その月のうちに出口へ抜ける分」、 が「別の状態へ移ってから行き着く分」です。導出も簡単で、 の に先ほどの展開 を入れると、 とまとまります。復帰の行・有料化の列について、これを書き下してみます。
B = N @ R # 行は定着, 休眠, 復帰。列は解約, 有料化
# 復帰から有料化へ行き着く 14.5% を、最初の1ヶ月で3つに分ける
direct = R[2, 1] # その月にそのまま有料化する
via_stay = Q[2, 0] * B[0, 1] # 定着へ戻り、そこから有料化に行き着く
via_dorm = Q[2, 1] * B[1, 1] # 休眠へ落ち、そこから有料化に行き着く
print(np.round(np.array([direct, via_stay, via_dorm]) * 100, 1))
# [5. 5. 4.5]
その月にそのまま有料化する分が 5.0 ポイント、いったん定着へ戻ってそこから有料化する分が 5.0 ポイント、また休眠に落ちて時間をかけて有料化する分が 4.5 ポイント。直行の分は3分の1ほどで、残りは他の状態を経由して積み上がっていることが読めます(いずれもダミーの確率を置いた上での話です)。
この計算があると、施策の比較を最終的な行き先で行えるようになります。休眠からの復帰率を上げる施策と、復帰ユーザーの定着率を上げる施策、どちらも遷移確率の1マスを動かす施策ですが、動かした行列で と の積を計算し直せば、有料化と解約の比率がどれだけ動くかまで見積もれます。
仮定と使いどころ
最後に、序盤に置いた2つの仮定に戻ります。実際のデータでは、マルコフ連鎖の仮定は完全には満たされません。推移確率は、季節性やプロダクトの改善によって動きます。翌月の状態が今月の状態だけで決まるという仮定も、厳密には怪しい。長く使い込んでから休眠したユーザーと、登録直後に休眠したユーザーでは、同じ「休眠」でも戻りやすさは違うはずです。
こうした個人差は、単に精度が少し落ちる、という話にとどまりません。離れやすい人は早い月に解約していなくなるので、生き残っている集団の中身は、時間が経つほど「残りやすい人」に入れ替わっていきます。同じコホートを追いかけると継続率が月を追うごとに上がって見えることが多いのは、このためです。ところがモデルは、この入れ替わりを知らず、初月の平均の解約率を全員にずっと当てはめ続けるので、後半の月では実際より速くユーザーを減らしてしまいます。その結果、解約までの期間は短めに出やすくなります。さきほどの 11.2 ヶ月も、下限寄りの数字だと思っておくくらいがちょうどいいはずです。
どれくらいずれるかは、手元のデータで測れます。前半の月だけを使って推移確率を作り、そこから後半を予測して、実際の構成と突き合わせればいい。当たらないかもしれない、で止めずに、どちら向きにどれだけ外すのかを一度見ておくと、その後の数字の受け取り方が変わります。
それでも、月次遷移の集計さえあれば「このままなら何が起きるか」を数字にできるのは、この道具ならではの強みです。出てくる数字は、当たる予測というより、現状の遷移構造をそのまま延長した場合の見通しです。まず現状の確率で解約までの平均期間を出しておき、施策で変えられそうな確率を動かして再計算し、どれだけ改善しうるかを見る。予測モデルとして信じ込むのではなく、この範囲で使うのが、仮定の粗さと釣り合う使い方だと思います。
参考
- ブラッドリー, I., ミーク, R. L. (著), 小林淳一, 三隅一人 (訳) (2014). 『社会のなかの数理[新装版]: 行列とベクトル入門』九州大学出版会.