
はじめに
過去10〜15年でデジタル技術が進歩し、マーケティング戦略は大きく変化しました。有名ブランドから小規模市場まで、取引、購買履歴、嗜好、購買力、購買活動、デモグラフィック、レビューなど、膨大なデータが収集されています。これらのデータは、購入意図から実際の購入、さらには常連顧客になるまで、さまざまな段階における顧客行動の理解に役立ちます。ここでデータサイエンスの可能性が活きてきます。
データサイエンスは、直感的には捉えにくい非自明な行動パターンや同時発生なども含め、マーケティングのビッグデータを実行可能な示唆に変換します。その結果、マーケターはターゲットオーディエンスの全体像をより明確に把握し、新規顧客を獲得しつつ既存顧客を維持し、マーケティング戦略を最適化し、企業の認知度を高め、より成功確度の高い広告キャンペーンを展開し、新たなチャネルを取り込み、ひいては企業の収益最大化につなげることができます。
マーケティングにおける典型的なデータサイエンスのユースケースのひとつが、顧客解約率(チャーン)の予測です。ここではこのトピックを詳しく見ていきます。
マーケティングのユースケース:顧客解約率の予測
顧客解約(チャーン)とは、利用していたサービスのサブスクリプションを解約し、そのサービスの顧客でなくなる傾向を指します。顧客解約率は、あらかじめ定めた期間内に解約した顧客の割合です。新規顧客の増加を追跡する顧客増加率とは反対の指標です。
顧客解約率は、顧客満足度や企業全体の健全性を測る非常に重要な指標です。どのビジネスにも常に存在する自然減や、特定サービスに見られる季節要因による解約に加えて、社内で何か問題が起きており是正が必要であることを示す要因もあります。たとえば次のような点です。
- 顧客サポートの不在または品質の低さ
- ネガティブな顧客体験
- より良い条件や価格戦略を持つ競合への乗り換え
- 顧客の優先順位の変化
- 長期顧客の不満足
- サービスが期待に達しなかった
- 金銭的な問題
- 支払いに対する不正防止措置の影響
高い解約率は、以下の理由から、いずれの企業にとっても深刻な問題です。
- 企業の収益減少と相関する。
- 新規顧客の獲得には、既存顧客の維持よりも多くのコストがかかる。これは特に競争の激しい市場で顕著。
- 顧客サービスの粗雑さが原因で解約が起きると、不満を抱いた元顧客がソーシャルメディアやレビューサイトに残す否定的な口コミにより、企業の評判が大きく損なわれうる。
サブスクリプション型サービスにおいて、顧客維持は事業戦略の要です。解約率を予測し、適切な予防策を講じるには、顧客行動(購入間隔、顧客期間、キャンセル、フォローアップの電話やメッセージ、オンライン活動)に関する情報を収集・分析し、離反リスクの高い顧客に特徴的な属性やその組み合わせを把握する必要があります。特に高収益または長期の顧客について、事前に離反の可能性を把握できれば、対象を絞って、引き留めのための有効な戦略を立てることができます。たとえば、該当顧客に連絡し、特典、割引、同価格でのプランアップグレード、その他のパーソナライズされた体験を提案する、といったアプローチが考えられます。
技術的には、顧客解約予測は典型的な機械学習の分類問題であり、離反リスクの有無を「yes」または「no」のラベルで判定します。ここではPythonを使い、実データでこのユースケースを検討します。
テレコムのビジネスモデルを想定し、顧客が1つの基本契約のもとで複数のサービスを利用する状況をモデル化します。データセットには、クリーニング済みの顧客活動の特徴量と、解約の有無を示すラベルが含まれます。
まずデータを確認し、解約率の分布を見てみましょう。
import pandas as pd
telcom = pd.read_csv('telco.csv')
print(f'Number of customers: {telcom.shape[0]:,}\n'
f'Churn values: {set(telcom['Churn'])}\n\n'
f'Churn distribution, %:\n{round(telcom.groupby(['Churn']).size()/telcom.shape[0]*100).convert_dtypes()}')
Number of customers: 7,032
Churn values: {0, 1}
Churn distribution, %:
Churn
0 73
1 27
dtype: float64
顧客の27%が解約しており、かなり高い水準です。ただし前回のデータサイエンスの事例と比べると、このデータセットでは深刻なクラス不均衡はなさそうです。
次に、機械学習手法で解約予測を行うための前処理を行います。データを学習用とテスト用に分割し、特徴量と目的変数を抽出します。
from sklearn.model_selection import train_test_split
target = ['Churn']
custid = ['customerID']
cols = [col for col in telcom.columns if col not in custid + target]
X = telcom[cols]
y = telcom[target]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
最初に用いるモデルは、シンプルなロジスティック回帰の分類モデルで、解約ラベルを予測し、精度を評価します。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
lr = LogisticRegression()
lr.fit(X_train, y_train)
predictions = lr.predict(X_test)
print(f'Test accuracy: {round(accuracy_score(y_test, predictions), 4)}')
Test accuracy: 0.8009
次に、ロジスティック回帰にL1正則化による特徴選択を加え、スケーリング後のデータで実行してみます。正則化の強さの逆数であるパラメータCの値は、モデル精度に影響します。ここではCを0.025に設定します。
lr = LogisticRegression(penalty='l1', C=0.025, solver='liblinear')
lr.fit(X_train, y_train)
predictions = lr.predict(X_test)
print(f'Test accuracy: {round(accuracy_score(y_test, predictions), 4)}')
Test accuracy: 0.7969
次に、L1正則化のCをチューニングし、モデルの複雑さを抑えつつ性能を維持できる最適値を探ります。Cのさまざまな値でロジスティック回帰を学習し、評価指標を計算します。
パラメータ候補を格納したリストCと、Cの値・非ゼロ係数数・精度の3列を持つ配列l1_metricsを用意しています。では実行結果を見てみましょう。
C Non-Zero Coeffs Accuracy
0 1.0000 23.0 0.801479
1 0.5000 22.0 0.799204
2 0.2500 21.0 0.802048
3 0.1000 20.0 0.802617
4 0.0500 18.0 0.802048
5 0.0250 13.0 0.796928
6 0.0100 5.0 0.790102
7 0.0050 3.0 0.783276
8 0.0025 2.0 0.745734
より小さいCでは非ゼロ係数(=モデリングに使われる特徴量)が減り、モデルの複雑さが下がる一方で、精度も低下することがわかります。C=0.05が最適に見えます。特徴量を18に減らしつつ、非正則化モデルをわずかに上回る精度を得ています。
次に、別のアルゴリズムである決定木モデルも試してみます。
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
print(f'Test accuracy: {round(accuracy_score(y_test, predictions), 4)}')
Test accuracy: 0.7275
過学習を避けつつより高精度なモデルを選ぶため、木の深さ(max_depth)をチューニングして最適値を特定します。先ほどのロジスティック回帰でCを選んだときと同様に、max_depthの候補を反復し、それぞれで決定木を学習して精度を計算します。
パラメータ候補を格納したdepth_listと、深さ候補と精度を格納する2列の配列depth_tuningを用意しています。では適用して最適な深さを見つけましょう。
depth_list = list(range(2, 15))
depth_tuning = np.zeros((len(depth_list), 2))
depth_tuning[:, 0] = depth_list
for index in range(len(depth_list)):
clf = DecisionTreeClassifier(max_depth=depth_list[index])
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
depth_tuning[index, 1] = accuracy_score(y_test, predictions)
col_names = ['Max_Depth', 'Accuracy']
print(pd.DataFrame(depth_tuning, columns=col_names))
Max_Depth Accuracy
0 2.0 0.756542
1 3.0 0.783276
2 4.0 0.782708
3 5.0 0.791809
4 6.0 0.778157
5 7.0 0.780432
6 8.0 0.757110
7 9.0 0.762230
8 10.0 0.763936
9 11.0 0.752560
10 12.0 0.745165
11 13.0 0.732651
12 14.0 0.727531
このように、深さを増やすと精度は一旦向上し、その後低下に転じます。max_depth=5で最も高い精度となるため、この値を最適な木の深さと見なせます。
ロジスティック回帰と決定木の両モデルで最適パラメータが分かったので、モデルを再構築し、解約率を押し上げたり抑えたりする主要因を特定・解釈します。
ロジスティック回帰では、得られた係数の指数を取り出して確認します。
# Reconstructing the best model
lr = LogisticRegression(penalty='l1', C=0.05, solver='liblinear')
lr.fit(X_train, y_train)
predictions = lr.predict(X_test)
# Combining feature names and coefficients into one dataframe
feature_names = pd.DataFrame(X_train.columns, columns=['Feature'])
log_coef = pd.DataFrame(np.transpose(lr.coef_), columns=['Coefficient'])
coefficients = pd.concat([feature_names, log_coef], axis=1)
# Calculating exponents of the coefficients
coefficients['Exp_Coefficient'] = np.exp(coefficients['Coefficient'])
# Removing coefficients that are equal to zero
coefficients = coefficients[coefficients['Coefficient']!=0]
print(coefficients.sort_values(by=['Exp_Coefficient']))
Feature Coefficient Exp_Coefficient
21 tenure -0.907750 0.403431
4 PhoneService_Yes -0.820517 0.440204
17 Contract_Two year -0.595271 0.551413
8 TechSupport_Yes -0.418254 0.658195
16 Contract_One year -0.414158 0.660896
5 OnlineSecurity_Yes -0.412228 0.662173
6 OnlineBackup_Yes -0.143100 0.866667
3 Dependents_Yes -0.039299 0.961463
7 DeviceProtection_Yes -0.017465 0.982687
11 PaperlessBilling_Yes 0.071389 1.073999
1 SeniorCitizen_Yes 0.097904 1.102857
19 PaymentMethod_Electronic check 0.188533 1.207477
22 MonthlyCharges 0.901454 2.463182
最も強く解約オッズに影響する特徴量はtenure(在籍期間)であることがわかります。一般に、係数の指数が1未満ならオッズを下げ、1を超えるとオッズを上げます。
決定木モデルでは、if-elseルールを抽出して可視化します。
# Reconstructing the best model
clf = DecisionTreeClassifier(max_depth=5)
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
from sklearn import tree
import graphviz
# Exporting a graphviz object from the trained decision tree
exported = tree.export_graphviz(decision_tree=clf,
out_file=None,
feature_names=cols,
precision=1,
class_names=['Not churn', 'Churn'],
filled=True)
graph = graphviz.Source(exported)
display(graph)

トップから始まる一連のif-elseルールとして解釈できる、見やすい決定木の可視化が得られました。ここでも、在籍期間が解約を左右する最重要変数であることが示されています。層を増やして木を深くすれば、他の変数についてもさらなる知見が得られます。
今後の展開としては、さらなるパラメータ調整、異なる学習・テスト分割の試行、他の機械学習アルゴリズムの適用と比較、複数の評価指標による性能評価などが考えられます。
顧客解約率の予測やマーケティングにおけるデータサイエンスの応用をより深く学びたい場合は、Machine Learning for Marketing in Pythonのコースから始めてみてください。