{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# XGBoost による顧客のチャーン予測\n", "_**勾配ブースティング木を使ったモバイル顧客の離脱率予測**_\n", "\n", "---\n", "\n", "---\n", "\n", "## Contents\n", "\n", "1. [Background](#Background)\n", "1. [Setup](#Setup)\n", "1. [Data](#Data)\n", "1. [Train](#Train)\n", "1. [Host](#Host)\n", " 1. [Evaluate](#Evaluate)\n", " 1. [Relative cost of errors](#Relative-cost-of-errors)\n", "1. [Extensions](#Extensions)\n", "\n", "---\n", "\n", "## Background\n", "\n", "_この Notebook は [AWS blog post](https://aws.amazon.com/blogs/ai/predicting-customer-churn-with-amazon-machine-learning/) とそれに付随する SageMaker Examples の和訳です_\n", "\n", "顧客を失うことはビジネスでは高く付きます。満足度の低い顧客を早い段階で特定することで、利用継続のインセンティブを与えられる可能性があります。この Notebook は機械学習 (ML) を用いて満足度の低い顧客を自動的に特定する方法 -- 顧客のチャーン予測 (customer churn prediction) とも呼ばれます -- を説明します。ML モデルが完璧な予測をすることはまれなので、この Notebook では ML を利用する経済的な結果を決める際の予測ミスの相対的なコストをどう取り入れるかについても書きます。\n", "\n", "ここでは我々にとって身近なチャーン、携帯電話事業者を解約する例を用いることにします (不満ならいつでも見つけられそうです。) もし通信会社が自分が解約しようとしていることを知っているなら、一時的なインセンティブ -- いつでも携帯をアップグレードできるとか、新しい機能が使えるようになるとか -- を与えて契約を継続させるでしょう。インセンティブは通常、失った顧客を再獲得するよりも圧倒的にコスト効率が良いのです。\n", "\n", "---\n", "\n", "## Setup\n", "\n", "_このノートブックの作成およびテストは ml.m4.xlarge ノートブックインスタンスを用いて行われました。_\n", "\n", "以下のものを用意して始めましょう: \n", "- トレーニングとモデルデータの置き場所として使う S3 バケットとプレフィックス。ノートブックインスタンス・学習・デプロイ場所と同じリージョンにある必要があります。\n", "- 学習・デプロイ用のコンテナがデータにアクセスするための IAM ロール ARN。これらの作成方法については[ドキュメント](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_create_for-service.html)を参照して下さい。_[注意] もしノートブックインスタンス、学習、デプロイの際に2つ以上のロールが必要な場合、boto regexp を適切な IAM ロール の ARN 文字列で置き換えて下さい。_" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "isConfigCell": true }, "outputs": [], "source": [ "# Define IAM role\n", "import boto3\n", "import re\n", "import sagemaker\n", "\n", "role = sagemaker.get_execution_role()\n", "sess = sagemaker.Session()\n", "bucket = sess.default_bucket()\n", "prefix = 'DEMO-xgboost-churn'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "次に、必要な Python ライブラリを import します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import io\n", "import os\n", "import sys\n", "import time\n", "import json\n", "from IPython.display import display\n", "from time import strftime, gmtime\n", "import sagemaker\n", "from sagemaker.predictor import csv_serializer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Data\n", "\n", "携帯電話事業者はどの顧客が解約し、誰がサービスを継続利用しているかの履歴データを持っています。この過去の情報を使って ML モデルを学習させることにより、ある事業者の解約率を予測するモデルを構築することができます。モデルを学習させた後、任意の顧客情報 (学習に使った情報と同様のもの) をモデルに入力することによって、顧客が解約しようとしているかを予測することができます。もちろん、モデルが予測を間違えることもあります -- 結局のところ、未来を予測することは一筋縄ではいかないということなのです。しかし、ここではその予測誤差とどう付き合っていくかについても言及します。\n", "\n", "今回扱うのは、Daniel T. Larose の [Discovering Knowledge in Data](https://www.amazon.com/dp/0470908742/) という本で述べられている公開データセットです。これは University of California Irvine (UCI) 機械学習レポジトリの著者に帰属します。さて、ダウンロードしてデータセットを見てみましょう: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!wget http://dataminingconsultant.com/DKD2e_data_sets.zip\n", "!unzip -o DKD2e_data_sets.zip" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn = pd.read_csv('./Data sets/churn.txt')\n", "pd.set_option('display.max_columns', 500)\n", "churn" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "最近の基準からすると比較的小さなデータセットで、とある US の携帯事業者の顧客データを 3,333 レコードと、それぞれ 21 個の属性で表しています。それぞれの属性は: \n", "- `State`: 顧客が住んでる US の州 (2文字の略号): OH, NJ など\n", "- `Account Length`: このアカウントがアクティブだった日数\n", "- `Area Code`: 顧客の電話番号に対応する3桁の地域コード\n", "- `Phone`: のこりの7桁の電話番号\n", "- `Int’l Plan`: 顧客が国際電話プランに契約しているかどうか: yes/no\n", "- `VMail Plan`: 顧客がボイスメール機能を使っているかどうか: yes/no\n", "- `VMail Message`: (恐らく) 月ごとのボイスメールの平均メッセージ数\n", "- `Day Mins`: 日中に使用された通話時間の合計数\n", "- `Day Calls`: 日中にかけられた電話の回数\n", "- `Day Charge`: 昼間の通話料金\n", "- `Eve Mins, Eve Calls, Eve Charge`: 夕方の通話料金\n", "- `Night Mins`, `Night Calls`, `Night Charge`: 夜間の通話料金\n", "- `Intl Mins`, `Intl Calls`, `Intl Charge`: 国際電話の通話料金\n", "- `CustServ Calls`: カスタマーサービスへの通話数\n", "- `Churn?`: 顧客が解約したかどうか: true/false\n", "\n", "最後の属性 `Churn?` はターゲット属性として知られています -- 我々が ML モデルに予測してほしい属性です。ターゲット属性が2値なので、我々のモデルは2値の予測 (2値分類) を行うよう設計します。\n", "\n", "それではデータを見てみましょう: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Frequency tables for each categorical feature\n", "for column in churn.select_dtypes(include=['object']).columns:\n", " display(pd.crosstab(index=churn[column], columns='% observations', normalize='columns'))\n", "\n", "# Histograms for each numeric features\n", "display(churn.describe())\n", "%matplotlib inline\n", "hist = churn.hist(bins=30, sharey=True, figsize=(10, 10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "すぐに以下のことが分かります: \n", "- `State` は一様に分布している。\n", "- `Phone` は実用的でない大量の一意な値を取る。プレフィックスを使うことはできるかもしれないが、どういう割当がされてるか分からないなら使わないほうがよさそう。\n", "- 顧客の 14% だけが解約している。2クラス間のデータ数に不均衡があるものの、それほど極端ではない。\n", "- ほとんどの数値特徴量は驚くほどいい感じに分布していて、釣り鐘型 -- ガウシアン的な分布をしている。`VMail Message` は特筆すべき例外 (そして `Area Code` は非数値型に変換すべき特徴量として現れている)。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn = churn.drop('Phone', axis=1)\n", "churn['Area Code'] = churn['Area Code'].astype(object)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "次はそれぞれの特徴量とターゲット変数間の関係を見てみましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for column in churn.select_dtypes(include=['object']).columns:\n", " if column != 'Churn?':\n", " display(pd.crosstab(index=churn[column], columns=churn['Churn?'], normalize='columns'))\n", "\n", "for column in churn.select_dtypes(exclude=['object']).columns:\n", " print(column)\n", " hist = churn[[column, 'Churn?']].hist(by='Churn?', bins=30)\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "面白いことにチャーンは: \n", "- 地理的に均等に分布している\n", "- 国際プランに入っている傾向がある\n", "- ボイスメールには入らない傾向にある\n", "- 日毎の利用時間が二峰性になっている (非解約者に比べて高いか低いかのどちらか)\n", "- 多くの顧客がカスタマーサービスを使っている (多くの問題を経験した顧客が解約しやすい、というのは理解できる)\n", "\n", "ように見えます。これに加えて、解約者は `Day Mins` や `Day Charge` のような特徴量について非常に似通った分布をしていることがわかります。通話時間は金額と相関するため特に驚きはないですね。それでは特徴量の関係についてもう少し深く見てみましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "display(churn.corr())\n", "pd.plotting.scatter_matrix(churn, figsize=(12, 12))\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "いくつかの特徴量は本質的に 100% の相関を持っていることが分かります。これらの特徴量ペアを含め、些細な冗長性やバイアスとして扱われるものもありますが、いくつかの機械学習のアルゴリズムには致命的な問題となる可能性があります。それぞれから高い相関を持つペアを取り除いてみましょう。`Day Mins` から `Day Charge` を、`Night Mins` から `Night Charge` を、`Intl Mins` から `Intl Charge` を除外します:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn = churn.drop(['Day Charge', 'Eve Charge', 'Night Charge', 'Intl Charge'], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "さて、これでデータセットの前処理が終わったので、どのアルゴリズムを使うかを決定しましょう。上で述べたとおり、特定の値が (中間ではなく) 高いか低いかのどちらかにあればチャーンと予測できそうなことが分かっています。これを線形回帰などのアルゴリズムに入れるためには、多項式の (bucketed) 項を作る必要があります。かわりにこの問題を勾配ブースティング木を使ってモデル化することを考えましょう。Amazon SageMaker はマネージドで、分散学習でき、リアルタイム推論エンドポイントをデプロイできる XGBoost コンテナを提供しています。XGBoost は、特徴量とターゲット変数間の非線形な関係を自然に説明し、特徴量間の複雑な関係も記述する勾配ブースティング木 (gradient tree boosting) を用いています [[KDD'16](https://dl.acm.org/citation.cfm?id=2939785)]。\n", "\n", "Amazon SageMaker XGBoost は CSV あるいは LibSVM フォーマットどちらのデータでも学習できます。この例では CSV にこだわります。データは\n", "- 予測変数を1列目にもつ\n", "- ヘッダ行をもたない\n", "\n", "必要があります。まずはじめにカテゴリ変数を数値に変換しましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model_data = pd.get_dummies(churn)\n", "model_data = pd.concat([model_data['Churn?_True.'], model_data.drop(['Churn?_False.', 'Churn?_True.'], axis=1)], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "そして、データを training, validation, test セットに分けましょう。これによりモデルの過学習を防ぐことができ、未知のデータを用いてモデルの精度をテストすることが可能になります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_data, validation_data, test_data = np.split(model_data.sample(frac=1, random_state=1729), [int(0.7 * len(model_data)), int(0.9 * len(model_data))])\n", "train_data.to_csv('train.csv', header=False, index=False)\n", "validation_data.to_csv('validation.csv', header=False, index=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "これらのファイルを S3 にアップロードします。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "boto3.Session().resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'train/train.csv')).upload_file('train.csv')\n", "boto3.Session().resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'validation/validation.csv')).upload_file('validation.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Train\n", "\n", "トレーニングに移ります。まず XGBoost アルゴリズムコンテナの場所を指定する必要があります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.amazon.amazon_estimator import get_image_uri\n", "container = get_image_uri(boto3.Session().region_name, 'xgboost', '0.90-1')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "次に、今回 CSV ファイルを使ってトレーニングを行うので、S3 に置いたファイルのポインターとして学習用の関数が使う `s3_input` を作ります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s3_input_train = sagemaker.s3_input(s3_data='s3://{}/{}/train'.format(bucket, prefix), content_type='csv')\n", "s3_input_validation = sagemaker.s3_input(s3_data='s3://{}/{}/validation/'.format(bucket, prefix), content_type='csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ここで、いくつかのパラメータ -- 学習に使うインスタンスの種類・数と XGBoost のハイパーパラメータ -- を指定します。いくつかの重要なパラメータは: \n", "- `max_depth` はアルゴリズム内でそれぞれの木がどれくらい深く作られるかをコントロールします。木が深いほどフィッティングは良くなりますが、計算量が多くなり過学習しやすくもなります。典型的にはモデルのパフォーマンスとトレードオフがあり、多数の浅い木と少数の深い木の間でパラメータを探索する必要があります。\n", "- `subsample` は学習データのサンプリングをコントロールします。これは過学習を防ぎますが、あまり低くしすぎるとデータ不足になります。\n", "- `num_round` はブースティングのラウンド数をコントロールします。これは本質的には前のイテレーションの残差を使って引き続きモデルを学習させます。これも、ラウンドを増やせば学習データに対するフィッティングは良くなりますが、計算量が増えたり過学習する可能性があります。\n", "- `eta` は各ブースティングラウンドのアグレッシブさを決定します。値が大きい方が保守的なブースティングになります。\n", "- `gamma` は木の成長がどれだけアグレッシブかを決めます。大きい値のほうが保守的なモデルを作ります。\n", "\n", "詳細は GitHub [page](https://github.com/dmlc/xgboost/blob/master/doc/parameter.rst) を読んで XGBoost のハイパーパラメータを確認して下さい。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sess = sagemaker.Session()\n", "\n", "xgb = sagemaker.estimator.Estimator(container,\n", " role, \n", " train_instance_count=1, \n", " train_instance_type='ml.m4.xlarge',\n", " output_path='s3://{}/{}/output'.format(bucket, prefix),\n", " sagemaker_session=sess)\n", "xgb.set_hyperparameters(max_depth=5,\n", " eta=0.2,\n", " gamma=4,\n", " min_child_weight=6,\n", " subsample=0.8,\n", " silent=0,\n", " objective='binary:logistic',\n", " num_round=100)\n", "\n", "xgb.fit({'train': s3_input_train, 'validation': s3_input_validation}) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Host\n", "\n", "それではモデルを学習させたので、エンドポイントにデプロイしましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xgb_predictor = xgb.deploy(initial_instance_count=1,\n", " instance_type='ml.m4.xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Evaluate\n", "\n", "これでエンドポイントが立ち上がったので、http POST リクエストを投げることでリアルタイム推論を非常に簡単に行うことができます。しかしまず、`test_data` の NumPy array をエンドポイントの裏のモデルに渡すために、シリアライザーとデシリアライザーを設定しなければなりません。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xgb_predictor.content_type = 'text/csv'\n", "xgb_predictor.serializer = csv_serializer\n", "xgb_predictor.deserializer = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "以下の機能を持った簡単な関数を作ります: \n", "1. test データセットをループする\n", "1. ミニバッチに分割する\n", "1. CSV string payload に変換する\n", "1. XGBoost エンドポイントを呼び出してミニバッチに対する予測を行う\n", "1. 予測値を集めてモデルから出力された CSV を NumPy array に変換する" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def predict(data, rows=500):\n", " split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))\n", " predictions = ''\n", " for array in split_array:\n", " predictions = ','.join([predictions, xgb_predictor.predict(array).decode('utf-8')])\n", "\n", " return np.fromstring(predictions[1:], sep=',')\n", "\n", "predictions = predict(test_data.as_matrix()[:, 1:])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "機械学習モデルのパフォーマンスを比較する多くの方法がありますが、単純に正しい値と予測値を比べてみましょう。この場合、単純に顧客が解約する (`1`) か、しない (`0`) かを予測することで、混同行列が得られます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.crosstab(index=test_data.iloc[:, 0], columns=np.round(predictions), rownames=['actual'], colnames=['predictions'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_[注意] アルゴリズムの乱択性により、結果が少し違って見えるかもしれません。_\n", "\n", "解約した人 48人のうち、39人を正しく予測することができました (true positive)。間違えて予測された4人はそのまま契約を続けるでしょう (false positive)。そのほか解約しないと予測された9人の顧客は実際に離脱しています (false negative)。\n", "\n", "ここで重要な点は `np.round()` 関数が原因で0.5という単純な閾値 (カットオフ値) を設定していることです。`xgboost` からの予測は0から1の間の連続値を取るので、それを元々の2クラスに戻して解釈しています。しかし、離脱する顧客は、解約しようとして企業がより積極的に引き留めようとする顧客よりもコストがかかることが期待されるので、このカットオフを調整する必要があります。これはほとんど確実に false positive を増やしますが、同時にtrue positive も増やし、false negative を減らします。\n", "\n", "ざっくりとした直観を得るために、予測結果の連続量を見てみましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.hist(predictions)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "モデルから出力された連続値は0と1の間ですが、0.1と0.9の間に十分な値があるため、cutoffを変えれば実際に予測される顧客数が変化するはずです。例えば、" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.crosstab(index=test_data.iloc[:, 0], columns=np.where(predictions > 0.3, 1, 0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "カットオフ値を0.5から0.3に変化させることで、もう1人の true positive と 3人の false positive、そして false negative が1人少なくなったことが分かります。数字は全体的に少ないですが、カットオフの変化により顧客の 6-10% に影響を与えています。これは正しい判断なのでしょうか?3人の顧客を繋ぎ止められますが、同時に5人に不要なインセンティブを与えていることになります。最適なカットオフを決めることは実世界に機械学習を応用する上で重要なステップになります。もう少し一般的に議論して、いくつかの仮定のもとでの解を考えましょう。\n", "\n", "### Relative cost of errors\n", "\n", "どんな2値分類問題も同じような感度のカットオフを生むことはありませんが、これ自体では特に問題とはなりません。結局、もし2クラスのスコアが十分簡単に分離可能なら、問題はそれほど難しくなく、ML ではなく単純なルールで問題を解くことが可能です。\n", "\n", "これより重要なのはもし ML モデルで推論させても、モデルが間違って false positive と false negative を割り振るコストがあることです。同じように、true positives と true negatives の正しい推論に付随したコストも見る必要があります。なぜなら、カットオフの選び方はこれらの統計量の4つ全てに関わるからで、各推論に対して4つの出力がどのようなビジネス上の相対コストを生むか考慮する必要があるからです。\n", "\n", "#### Assigning costs\n", "\n", "今回問題にした携帯事業者の解約におけるコストとは何でしょうか? もちろんコストはビジネス上で取りうる具体的なアクションに依存します。ここでいくつかの仮定を置いてみましょう。\n", "\n", "はじめに、true negative のコストを ¥0 (\\*) とします。我々のモデルは本質的に、この場合幸せな顧客正しく特定できていることになるので、何もする必要はありません。\n", "\n", "次に false negative は一番問題で、離脱しそうな顧客が留まると間違って予測するからです。顧客を失い、放棄所得、広告コスト、管理コスト、店頭コスト、そして恐らく携帯電話ハードウェア補助金を含む、代わりの顧客を獲得するコストを支払う必要があります。\n", "\n", "最後に、解約しそうとモデルが予測した顧客については、リテンションのためのインセンティブが仮に一万円だとしましょう。もし事業会社が自分にこういう譲歩をしてきたら、さすがに解約までにもう一回考え直すでしょう。これが true positive と false positive の結果に対するコストです。ここで false posivie (顧客は満足だが誤って離脱と予測) の場合、一万円を _ドブに捨てる_ ことになります。恐らくこの一万円をもっと賢く使うことはできたでしょうが、既存の顧客の支持を高める可能性もあるため、それほど悪い選択ではありません。\n", "\n", "\\* 以下 \\$1 = ¥100 というレートで訳すことにします" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Finding the optimal cutoff\n", "\n", "false negatives が false positives よりも相当コストがかかるのは明らかです。顧客数をもとにエラーを最適化するかわりに、このようなコスト関数を最小化しましょう: \n", "\n", "```txt\n", "$500 * FN(C) + $0 * TN(C) + $100 * FP(C) + $100 * TP(C)\n", "```\n", "\n", "ここで `FN(C)` は false negative がカットオフ `C` の関数であることを意味し、`TN, FP, TP` についても同様です。上の式の結果を最小化するカットオフ `C` を探す必要があります。\n", "\n", "これを行う素直な方法は、複数の考えられるカットオフについてシミュレーションを走らせることです。以下では100通りのカットオフについてforループで計算を行います。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cutoffs = np.arange(0.01, 1, 0.01)\n", "costs = []\n", "for c in cutoffs:\n", " costs.append(np.sum(np.sum(np.array([[0, 100], [500, 100]]) * \n", " pd.crosstab(index=test_data.iloc[:, 0], \n", " columns=np.where(predictions > c, 1, 0)))))\n", "\n", "costs = np.array(costs)\n", "plt.plot(cutoffs, costs)\n", "plt.show()\n", "print('Cost is minimized near a cutoff of:', cutoffs[np.argmin(costs)], 'for a cost of:', np.min(costs))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "上の図は、閾値を低くしすぎると全顧客に対してリテンションインセンティブを渡すことになりコストが跳ね上がることを示しています。一方で、閾値を高くしすぎるとあまりに多くの顧客を手放すことになり最終的に同じぐらいコストがかかります。全体のコストはカットオフが 0.46 のときに最小の 84万円 になり、何もしない場合に 200万円 以上失うのよりは圧倒的に良いという結果になります。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Extensions\n", "\n", "このノートブックでは顧客が離脱するのを予測するモデルを構築する方法と、true/false positive と false negative により生じるコストを最適化する閾値の決め方について披露しました。これを拡張するにはいくつかの方法が考えられます: \n", "- リテンションインセンティブを受け取る顧客も離脱の可能性がある。インセンティブを受け取っても解約する確率を含めるとリテンションプログラムのROIが向上する。\n", "- 低価格のプランに移行したり課金オプションを無効化する顧客は別の種類のチャーンとしてモデル化できる。\n", "- 顧客行動の発展をモデル化する。もし使用量が落ちてカスタマーサービスへの連絡回数が増えている場合、解約される可能性が高い。顧客情報は行動傾向を取り入れるべきである。\n", "- 実際の学習データと金銭的コストはもっと複雑になり得る。\n", "- それぞれのチャーンに合わせた複数のモデルが必要。\n", "\n", "これらの複雑さに関わらず、このノートブックで説明したものと似たような原理は適用できるはずです。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Optimizing model for prediction using Neo API\n", "SageMaker Neo API を用いれば学習済みのモデルを特定のハードウェア用に最適化することが可能です。`compile_model()` 関数を呼ぶ際に、ターゲットとなるインスタンスファミリー (ここでは C5) とコンパイル済みのモデルを保存する S3 バケットを指定します。\n", "\n", "** [重要] もし以下のコマンドが permission error になる場合、ノートブックの上部にスクロールして `get_execution_role()` により返される execution role の値を確認して下さい。このロールには ``output_path`` で指定される S3 バケットへのアクセス権限が必要です。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "output_path = '/'.join(xgb.output_path.split('/')[:-1])\n", "compiled_model = xgb.compile_model(target_instance_family='ml_c5', \n", " input_shape={'data':[1, 69]},\n", " role=role,\n", " framework='xgboost',\n", " framework_version='0.7',\n", " output_path=output_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating an inference Endpoint\n", "\n", "コンパイル済みのモデルをデプロイすることができます (コンパイルのターゲットと同じインスタンスを指定する必要があります)。この操作により、推論のための SageMaker エンドポイントが作成されます。\n", "\n", "``deploy`` 関数の引数はエンドポイントに使われるインスタンス数とタイプを指定することができます。コンパイル時に指定されたインスタンスを選択して下さい (今回は `ml_c5` )。Neo API は Deep Learning Runtime (DLR) と呼ばれる特別なランタイムを使い最適化されたモデルを走らせます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# known issue: need to manually specify endpoint name\n", "compiled_model.name = 'deployed-xgboost-customer-churn'\n", "# There is a known issue where SageMaker SDK locates the incorrect docker image URI for XGBoost\n", "# For now, we manually set Image URI\n", "compiled_model.image = get_image_uri(sess.boto_region_name, 'xgboost-neo', repo_version='latest')\n", "compiled_predictor = compiled_model.deploy(initial_instance_count = 1, instance_type = 'ml.c5.4xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Making an inference request\n", "コンパイルされたモデルは CSV の入力を受け付けます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "compiled_predictor.content_type = 'text/csv'\n", "compiled_predictor.serializer = csv_serializer\n", "compiled_predictor.deserializer = None" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def optimized_predict(data, rows=500):\n", " split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))\n", " predictions = ''\n", " for array in split_array:\n", " predictions = ','.join([predictions, compiled_predictor.predict(array).decode('utf-8')])\n", "\n", " return np.fromstring(predictions[1:], sep=',')\n", "\n", "# Batch prediction is not supported yet; need to send one data point at a time\n", "dtest = test_data.as_matrix()\n", "predictions = []\n", "for i in range(dtest.shape[0]):\n", " predictions.append(optimized_predict(dtest[i:i+1, 1:]))\n", "predictions = np.array(predictions).squeeze()\n", "predictions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### (Optional) Clean-up\n", "\n", "このノートブックによって作られたリソースを削除していい場合、以下のセルを実行してください。このコマンドは上で作成したエンドポイントを削除して意図しない請求を防ぐことができます。\n", "(必要であれば、このノートブック自体を走らせているノートブックインスタンスも SageMaker のマネージメントコンソールから停止させて下さい。)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sagemaker.Session().delete_endpoint(xgb_predictor.endpoint)\n", "sagemaker.Session().delete_endpoint(compiled_predictor.endpoint)" ] } ], "metadata": { "kernelspec": { "display_name": "conda_python3", "language": "python", "name": "conda_python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.5" }, "notice": "Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the \"License\"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the \"license\" file accompanying this file. This file is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." }, "nbformat": 4, "nbformat_minor": 4 }