HAL_DATA_techBlog

HALDATAの技術ブログとリリース情報です。

Hivemallを利用したLTV予測

はじめに

本ブログ記事は、以下の読者を想定しております。

  • TreasureDataにサイトのアクセス履歴など、学習に利用できるデータを保持している方
  • TreasureDataに蓄積されたデータの活用方法に困っている方

本ブログ記事では、Webのアクセス履歴や商品の購入履歴をHivemallを利用して学習し、ユーザのライフタイムバリュー(以降LTV)を予測する流れをまとめます。 また、今回の内容は、Treasure Data様が公開している”Hivemall を利用した機械学習実践入門(第一回:ドラッグストアのセールス予測)”を応用したものになります。

今回の目的

TreasureDataをDMPとしてデータを集約すると、データの活用を当然考えたくなると思います。その活用の一つとして、機械学習を用いた将来予測があります。 Hivemallは、TreasureDataで利用できる機械学習ライブラリです。今回は、このHivemallを用いて、LTVの高いロイヤルカスタマーの予測を行ってみましょう。

推測に用いるデータ

今回は、以下のような会員情報と商品の購入履歴を想定します。

会員情報

カラム名 意味
member_id 会員番号のUID
regist_date 会員登録日
zip 郵便番号
pref 都道府県
sex 性別
birth 誕生日
regist_reason 会員登録の理由
devicetype バイス種別
last_login_date 最終ログイン日
device_name 利用デバイス

購入履歴

カラム名 意味
order_id 購入履歴のID
order_date 購入日
total 合計購入金額
member_id 購入者の会員番号UID

上記のような情報から、推測に会員の要素と、推測の対象のLTVを定義していきます。

まず、LTVは、”会員登録から1年間の購入合計金額”と定義します。また、LTVを予測できそうな要素として、会員情報の要素と、購入後の初回購入金額を”first_sales”として利用してみます。

上記の購入履歴と会員情報を集計して学習に利用するデータを用意します。

学習に利用するデータ

カラム名 特性 意味
member_id 質的変数 会員番号のUID
ltv 目的変数/量的変数 会員登録日から1年間の合計購入金額
regist_date 量的変数 会員登録日
first_sales 量的変数 初日購入金額
zip 質的変数 郵便番号
pref 質的変数 都道府県
sex 質的変数 性別
birth 質的変数 誕生日
regist_reason 質的変数 会員登録の理由
devicetype 質的変数 バイス種別
last_login_date 量的変数 最終ログイン日
device_name 質的変数 利用デバイス
  • 量的変数: 値の間に連続性のある変数(例: 100円、150円、200円)
  • 質的変数: 値の間に連続性のない変数(例: 男、女、未回答)
  • 目的変数: 推測の目的となる変数

学習に利用するモデル

今回は、”Hivemall を利用した機械学習実践入門(第一回:ドラッグストアのセールス予測)”と同じく、”Random Forest”をモデルとして利用します。

Hivemallを利用した学習

今回は、”学習に利用するデータ”がTreasureDataのテーブルとして、用意できている前提で、以下の流れでHivemallを利用した学習モデルの作成を行います。

f:id:EggStore:20200226230445p:plain

(1) ”学習に利用するデータ”を一部をtrainテーブルとして抽出

(2) trainテーブルの特徴ベクトルを作成し、train2テーブルに保存

(3) train2テーブルを利用して、Random Forestモデルを作成し、modelテーブルに保存

また、学習結果から、どの要素がLTVに大きな影響を与えると学習したかを可視化します。

(4) 予測に影響が大きい要素をmodelテーブルから可視化

さらに、実際にできたモデルの以下の流れで実施に予測に利用して精度を確認します。

(5) ”学習に利用するデータ”の残りをtestテーブルとして抽出

(6) testテーブルの特徴ベクトルを作成し、test2テーブルに保存

(7) test2テーブルのLTV以外の情報から、Random Forestモデルを利用してLTVを予測

以降、上記の流れの詳細について説明していきます。

(1)”学習に利用するデータ”を一部をtrainテーブルとして抽出

”学習に利用するデータ”が100万レコード用意できたとしたら、その内90万行を学習に利用します。学習に利用する90万行は、”member_id”のハッシュ値をとって、ランダムに抽出します。学習に利用しない10万行は、学習モデルの予測精度評価に利用します。 ”学習に利用するデータ”を”member”テーブルに入れている場合は、以下のようなSQLになります。

create table train as
(
select * from members_all order by TD_MD5(CAST( member_id AS varchar(10) )) limit 900000
);

(2)trainテーブルの特徴ベクトルを作成し、train2テーブルに保存

trainテーブル内の質的変数を数値に変換するためHivemallに用意された”quantify”関数を利用します。 また、値がNULLの項目は、-1とします。 さらに、推測するLTVは値を小さくするため”LN”関数で対数をとります。これは、参考ブログと同じく対数をとった場合、そのままの場合で交差検定を行ったところ精度が良かったためです。そのまま扱う方が良い場合もあります。 最後に、Hivemallの関数に適用できる形に、テーブルを整形してtrain2テーブルに保存します。

WITH train_ordered as (
  select * from train
  order by member_id asc
), 
train_quantified as (
  select
    t0.member_id,
    t0.ltv,
    t0.first_sales,
    unix_timestamp(t0.birth) as birth_timestamp,
    unix_timestamp(t0.last_login_date) as last_login_date_timestamp,
    t2.*
  from
    train_ordered t0 
    -- indexing non-number columns
    LATERAL VIEW quantify(true,
          zip, pref, sex, regist_reason, devicetype, device_name) t2
       as zip, pref, sex, regist_reason, devicetype, device_name
)
INSERT OVERWRITE TABLE train2
SELECT
  t1.member_id,
  ARRAY( -- padding zero for missing values
    t1.birth_timestamp, t1.last_login_date_timestamp,t1.first_sales,
    IF(t1.zip IS NULL, -1, t1.zip), 
    IF(t1.pref IS NULL, -1, t1.pref), 
    IF(t1.sex IS NULL, -1, t1.sex),
    IF(t1.regist_reason IS NULL, -1, t1.regist_reason),
    IF(t1.devicetype IS NULL, -1, t1.devicetype),
    IF(t1.device_name IS NULL, -1, t1.device_name)
  ) AS features,
  LN(1 + t1.ltv) AS label, -- log scale conversion
  t1.ltv
FROM
  train_quantified t1
;

(3)train2テーブルを利用して、Random Forestモデルを作成し、modelテーブルに保存

Hivemallで用意された”randomforest_regressor”関数を利用して、100本の決定木で構成されたRandom Forestモデルを作成します。引数には、特徴ベクトル、目的変数、Random Forestの木の数と特徴ベクトルの変数種別を与えます。本来、一回のtrain_randomforest_regressorで100本決定木を作っても良いのですが、下記のように20本ずつ作ってunion allを使うことで、5並列に学習を行うことができます。

INSERT OVERWRITE TABLE model 
  SELECT 
    -- C: Categorical Variable, Q: Quantitative Variable
    train_randomforest_regressor(features, label, '-trees 20 -attrs Q,Q,Q,C,C,C,C,C,C')
  FROM
    train2
  UNION ALL
  SELECT 
    train_randomforest_regressor(features, label, '-trees 20 -attrs Q,Q,Q,C,C,C,C,C,C')
  FROM
    train2
  UNION ALL
  SELECT 
    train_randomforest_regressor(features, label, '-trees 20 -attrs Q,Q,Q,C,C,C,C,C,C')
  FROM
    train2
  UNION ALL
  SELECT 
    train_randomforest_regressor(features, label, '-trees 20 -attrs Q,Q,Q,C,C,C,C,C,C')
  FROM
    train2
  UNION ALL
  SELECT 
    train_randomforest_regressor(features, label, '-trees 20 -attrs Q,Q,Q,C,C,C,C,C,C')
  FROM
    train2;

(4)予測に影響が大きい要素をmodelテーブルから可視化

学習結果が入ったmodelテーブルを解析して、どのような要素がLTVが大きいという判定に利用されているか可視化します。可視化にはjupyter notebookを利用します。以下のようなプログラムを用意して、TDのアクセスキーを埋め込めば、棒グラフとして影響が大きい要素が表示されます。

!pip install pandas_td

%matplotlib inline

import os
import pandas as pd
import pandas_td as td
import matplotlib.pyplot as plt

con = td.connect(apikey='XXXXXXXXXXXXXXXXXXXX',endpoint='https://api.treasuredata.com/')
hive = con.query_engine(database='tutorial_hivemall', type='hive')

var_imp=td.read_td('''
WITH var_imp AS(
  SELECT 
    array_sum(var_importance) AS var_importance
  FROM
    model
)
SELECT 
  var_importance[0] AS birth,
  var_importance[1] AS last_login_date,
  var_importance[2] AS first_sales,
  var_importance[3] AS zip,
  var_importance[4] AS pref,
  var_importance[5] AS sex,
  var_importance[6] AS regist_reason,
  var_importance[7] AS devicetype,
  var_importance[8] AS device_name
FROM
  var_imp
''', hive)

X=range(var_imp.shape[1])
plt.barh(X,var_imp.values[0],align='center')
plt.yticks(X,var_imp.columns)
plt.savefig('var_imp.png')

例えば、以下のようなグラフが表示された場合は、初回の購入金額、誕生日、最終ログイン日が、LTVの予測に重要度が高いとわかります。

f:id:EggStore:20200222092605p:plain

(5)”学習に利用するデータ”の残りをtestテーブルとして抽出

学習に利用しなかった、10万レコードはハッシュ値の逆順で抽出します。

create table test as
(
select * from members_all order by TD_MD5(CAST( member_id AS varchar(10) )) desc limit 100000
);

(6)testテーブルの特徴ベクトルを作成し、test2テーブルに保存

ここの基本的な処理は、”2.trainテーブルの特徴ベクトルを作成し、train2テーブルに保存”と同じです。 ただし、testテーブルのみからquantify関数を使って採番すると、train2と採番対応が違ってしまう可能性があります。 そこで、trainテーブルをtestテーブルの先頭につけて、quantify関数を実行して、後でtrainテーブルを行を捨てます。 trainテーブルの列削除は、前回はtrueを与えていたquantify関数の第一引数に、"output_row"というtrainとtestを識別する列を与えることで実現しています。

WITH train_test as (
  select
    1 as train_first, false as output_row,
    member_id, ltv, first_sales, birth, last_login_date, zip, pref, sex, regist_reason, devicetype, device_name
  from
    train
  union all
  select
    2 as train_first, true as output_row,
    member_id, ltv, first_sales, birth, last_login_date, zip, pref, sex, regist_reason, devicetype, device_name
  from
    test
),
train_test_ordered as (
  select * from train_test
  order by train_first asc, member_id asc
), 
test_quantified as (
  select
    t0.member_id,
    t0.ltv,
    t0.first_sales,
    unix_timestamp(t0.birth) as birth_timestamp,
    unix_timestamp(t0.last_login_date) as last_login_date_timestamp,
    t2.*
  from
    train_test_ordered t0 
    -- indexing non-number columns
    LATERAL VIEW quantify(output_row,
          zip, pref, sex, regist_reason, devicetype, device_name) t2
       as zip, pref, sex, regist_reason, devicetype, device_name
)
INSERT OVERWRITE TABLE test2
SELECT
  t1.member_id,
  ARRAY( -- padding zero for missing values
    t1.birth_timestamp, t1.last_login_date_timestamp,t1.first_sales,
    IF(t1.zip IS NULL, 0, t1.zip), 
    IF(t1.pref IS NULL, 0, t1.pref), 
    IF(t1.sex IS NULL, 0, t1.sex),
    IF(t1.regist_reason IS NULL, 0, t1.regist_reason),
    IF(t1.devicetype IS NULL, 0, t1.devicetype),
    IF(t1.device_name IS NULL, 0, t1.device_name)
  ) AS features,
  LN(1 + t1.ltv) AS label, -- log scale conversion
  t1.ltv
FROM
  test_quantified t1
;

(7)test2テーブルのLTV以外の情報から、Random Forestモデルを利用してLTVを予測

test2テーブルとmodelテーブルをHivemallで用意された”tree_predict”関数に与えることで、LTVの予測値を取得することができます。複数の決定木で推測された値の平均をLTVの予測値とします。group byを使う都合上、本来のLTVの平均を計算していますが、全て同じ値なので結果は変わりません。 最後に、特徴ベクトル作成時に、LTVを対数に変換していたため、”EXP”関数で元の値に戻します。

INSERT OVERWRITE TABLE prediction
SELECT 
  member_id,
  EXP(predicted)-1 as predicted,
  ltv
FROM(
  SELECT
     member_id,
     avg(predicted) AS predicted,
     avg(ltv) AS ltv
  FROM(
    SELECT
      t.member_id,
      tree_predict(p.model_id, p.model, t.features, false) as predicted
      t.ltv
    FROM
      model p
      LEFT OUTER JOIN test2 t
  ) t1
  group by
    member_id
) t2;

上記を実行すると以下のようなpredictedテーブルを得ることができます。

f:id:EggStore:20200226233643p:plain

以上で、trainのデータから学習したRandom Forestモデルで、testのデータのLTVを推測することができました。

推測精度の評価

先の流れで、一連のHivemallのRandom Forestモデルを用いた推測を行うことができました。ここから学習に使うデータやモデルの最適化を行って、推測の精度を上げていくことになるかと思います。 今回は、”RMSPE(Root Mean Square Percentage Error”という評価指標で、実際のLTVと予測LTVの解離を評価します。

f:id:EggStore:20200226235439p:plain

SQLは以下になります。

SELECT
sqrt( AVG( POW((prediction.predicted - prediction.ltv)/prediction.ltv, 2) ) )
FROM prediction;

上記のSQLから、今回は”1.2027529136957467e”という値を得ました。ここから、この値が0に近づく学習モデルを目指します。

まとめ

HivemallのRandom Forestモデルを用いたお客様毎のLTVの推測を行いました。LTV以外にも、上記の流れで値の推測を行うことができるので、皆さんも蓄積されたデータの活用をご検討ください。

参考資料