トレジャーデータ(Treasure Data)ブログ

トレジャーデータ(Treasure Data)ブログです。

本サイトは移転しました。新サイトにリダイレクトします。

"); // リダイレクト   setTimeout("redirect()", 0); // 0 sec   function redirect(){     location.href = url;   }    // canonical の書き換え   var link = document.getElementsByTagName("link")[0];   link.href = url; -->

Hivemallを利用した機械学習実践入門 (第一回: ドラッグストアのセールス予測)

本記事は移転しました。新サイトにリダイレクトします。

"); // リダイレクト   setTimeout("redirect()", 0); // 0 sec   function redirect(){     location.href = url;   }    // canonical の書き換え   var link = document.getElementsByTagName("link")[0];   link.href = url; -->

本特集では、Treasure Data環境で利用可能な機械学習ライブラリHivemallを利用した機械学習の実践方法を紹介します。世界のデータサイエンティストが腕を競うデータサイエンスコンペティションサイトKaggleの中から、実践的な課題を扱っていきます。

1. はじめに

 第一回は小売業の売り上げ予測するタスクであるRossmann Store Salesコンペティションを課題に用います。アルゴリズムとしては、決定木を利用したアンサンブル学習手法の一種であるRandom Forest回帰を利用します*1。

 Rossmannはヨーロッパの7カ国で3,000以上の店舗を展開する薬局チェーンです。各店舗のマネージャーは6週間先までの店舗の売り上げを予測することがタスクとして課されています。各店舗の売り上げはプロモーション活動、競合要素、学校の休みや祝日、季節性、地域性など様々な要因に左右されます。

 Rossmann Store Salesコンペでは、Rossmannがドイツに展開する1,115店舗の売り上げ6週間分について1日の売り上げを予測することを目標としたデータが提供されています。訓練データとして各店舗における2013年1月1日から2015年7月31日までのデータが、検証用データとして2015年8月1日から2015年9月17日までのデータが与えられています。

 それでは、実際にどのようにTreasure Data上でデータを扱うのか解説していきます。

2. データの準備

2.1 データの投入

 Rossmann Store Salesタスクでは、訓練データ (train.csv)、検証用データ (test.csv)、そして、訓練/検証用データで用いる店舗情報 (store.csv)がCSV形式で提供されています。店舗情報はStore IDによって訓練データと検証用データと紐付きます。

 まず、Treasure Dataへのデータの投入をのGUIから行います。提供されているCSVデータからテーブルの作成は、ドラッグアンドドロップで簡単に行うことができます。Treasure Dataのファイルインポートでは、CSVの一行目のヘッダ情報と実際のデータ列を見て、テーブル定義が自動作成されます*2

 各データの構成は次のとおりです。

訓練データ (train.csv)
f:id:myui:20160308142753p:plain

検証用データ (test.csv)
f:id:myui:20160308142754p:plain

店舗情報 (store.csv)
f:id:myui:20160308142750p:plain

 各データ項目は次のようなものです。

Id 検証用 検証用データにのみ割り当てられていて店舗と日によって異なる
Sales 目的変数/量的変数 予測対象のある店舗における一日の売り上げ
Store 質的変数 各店舗に割り当てられた固有のId
Customers 量的変数 ある日に店舗を訪れた客数*3
Open 質的変数 0 のとき閉店、 1 のとき開店
StateHoliday 質的変数 a = public holiday, b = Easter holiday, c = Christmas, 0 = None
SchoolHoliday 質的変数 その店舗の営業日が、公立の学校の休業日の影響を受けるかを示している
StoreType 質的変数 店舗の形態( a, b, c, d) の4 種類
Assortment 質的変数 分類( a = basic, b = extra, c = extended)
CompetitionDistance 量的変数 最近接の競合店舗までの距離
CompetitionOpenSince[Month/Year] 質的変数 最近接の競合店舗が開店した月/年
Promo 質的変数 プロモーションをしているかどうか
Promo2 質的変数 複数の店舗におけるプロモーションで、 0 は不参加店舗、1 は参加店舗
Promo2Since[Year/Week] 質的変数 その店舗がPromo2 を始めた年/週
PromoInterval 質的変数 Promo2 を始める時期で、例えば、"Feb,May,Aug,Nov" の場合は異なるプロモーションを2月、5月、8月、11月に開始したことを意味する

2.2 データの傾向を可視化

 参考までにStore ID=1の訓練データの期間における売り上げを見てみましょう。このスクリプトでTD-pandasとJupyter Notebookを利用して可視化します*4。

f:id:puyokw:20160225151711p:plain

 データを視覚化することで、売り上げの周期性や売り上げのスパイクを確認することができます。視覚化することで、周期性を考慮できる特徴量(曜日、週末か平日かなど)があるとよいなどが推測が付きます。

2.3 データの前処理

2.3.1 データの結合

 まず、訓練データに店舗情報(プロモーションや競合する店などの情報が含まれる)を結合します。このようなデータの結合処理をTreasure Dataサービスは分散処理によって効率的に実行することができます。

 なお、日付のデータがyyyy-mm-dd(例えば、2014-05-25 など)の形式で与えられているため、年、月、日を部分文字列として抽出します。また、評価時に店舗が休みなどで売り上げがゼロのケースは予測対象から除外されているため、事前に訓練データからも取り除きます。

 上記のHiveクエリを実行して作成されるtraining2テーブルの内容は次のようになります。

f:id:puyokw:20160225175957p:plain

2.3.2 特徴ベクトルの生成

 前のステップで作成したテーブルにはtraining2には、非数値データが含まれています。本ステップでは次のようなクエリを用いて、非数値データをRandomForestで扱えるように数値データに変換した上で、特徴ベクトル(数値で表された特徴量の配列)を作っていきます*5。

 train_quantifiedの中のquantify関数で数値列はそのまま、非数値列に採番(数値ID付け)した上で出力しています*6。また、欠損値を0で補完し、目的変数となる売上データsalesはlogスケールを取ります*7。

 このように変換してできたテーブルは次のようになります。

2.3.3 検証用データの準備

 検証用データも同様に変換していきます。まずは、店舗情報のテーブルを検証用データに結合します。

 変換結果のテーブルは次のような形です。

f:id:myui:20160315212547p:plain

 次に、質的変数を量的変数に変換する際にトレーニングデータと同様な変換をする必要があることに注意して、検証用データを加工します。例えばStoreTypeはa, b, c, dの4 種類ありますが、トレーニングデータでa → 1, b → 2, c → 3, d → 4であるのに対して、検証用データにStoreTypeがb → 1, c → 2, d → 3となってしまっては困る*8ためです。

 最終的な変換結果の検証用テーブルは次のような形です。

f:id:myui:20160310185326p:plain

3. Random Forestを利用した学習

 前回までのステップで作成した訓練データとし、Random Forestを用いた学習を行います。Random Forestは異なる条件で構築した複数の決定木利用した集団学習手法ですが、今回は100本の決定木を構築するものとします。

 学習を行うクエリは次のとおりです。ここでは、UNION ALL句を用いることで5並列*9で決定木の学習を行っています。"-attr"オプションには変数が質的変数なものにはC、量的変数にはQを指定します*10。ここでは、competitiondistanceカラムのみを量的変数として指定しています。


3.1 変数重要度

 RandomForestの学習結果より、各説明変数の重要度を取得可能です。ここでは、Treasure Dataでの実行結果をJupyter notebook/Pandasと連携することで可視化します*11。

f:id:myui:20160315154656p:plain

 棒グラフより、店舗IDと競合店舗からの距離が各店舗の売り上げに影響する要素が高い変数であることが分かります。当然店舗ごとに売り上げが大きくことなるため、店舗IDが最も支配的な説明変数であることは直感にも合致します。競合店舗と距離が近いと売り上げを食い潰し合っているのでしょうか*12。

 なお、可視化に利用したJupyter notebookは以下を参照ください。


4. 予測

 先ほど作成したモデルを使用して予測を行います。学習時に目的変数をLN(1 + t1.sales)としてスケールを変換したため、EXP(predicted-1)でスケールの逆変換をしています。

 Kaggleへの予測結果の提出用にIDの昇順に結果をソートします。


5. 評価

 評価指標は、RMSPE(Root Mean Square Percentage Error)、すなわち
 \displaystyle RMSPE= \sqrt  { \frac{1}{n} \sum_{i=1}^{n}  ( \frac{ {y_i} - { \hat{y_i} } }{y_i} )^2 }
で行われます。ここで、 {y_i} はある店舗における1 日の売り上げ ,  \hat{y_i} はその予測値です。

 なお、交差検定に利用したコードはこちらです。

5.1. Kaggleへの結果の提出

 KaggleのPublic Leader Board(LB)では検証用データの39% を使用し、Private LBは残りの61%のデータを使用して評価が行われます。Public LBのスコアは、競技中に公開され、最終評価はそれまで非公開のPrivate LBで行われます。今回のモデルでは、Public LB: 0.12771、Private LB: 0.13763となりました*13。

 RandomForestはその名のとおり、乱数を利用するため、試行によっては予測性能は多少前後することがありますので、ご留意ください*14。

6. さいごに

 Kaggleはデータ分析の基礎を学ぶのに最適な場所です。今回取り上げた内容はeコマースや広告キャンペーンの売り上げ予測等にも応用が効くはずです。本記事の内容を追うことでTreasure Data上でのデータ分析/機械学習実行の基本を抑えることができるはずですので、興味のある方は是非お試し下さい。

 なお、Treasure Dataでは、DigdagというWorkflowエンジンを開発中です。今回のワークフローは次のようなYAML形式でworkflowを定義することによって一括実行やScheduled実行が可能となります。煩雑になりがちなデータ分析のワークフローもこのような形にすると見通しが良くなります。

 予告: 特集記事第二弾はディスプレイ広告業界で有名なCriteoのデータセットを使った広告クリック率推定の入門記事を予定しております。


*1:Random Forestを利用した理由は、本タスクで特徴エンジニアリングに手間をかけずとも比較的高い精度が期待できるためです。

*2:ただし、stateholidayカラムは先頭数行が数字のみであったためlongと判定されてしまいましたが、実際には非数値データも含まれていたためstring型に列の型を手動変更しました。

*3:訓練データのみの存在するため、今回は説明変数として利用しない

*4:今回の一店舗の売上げを可視化しましたが、売り上げの全体平均などを可視化するなどもクエリベースで簡単にできます。

*5:scikit-learnでいうDictVectorizer相当の処理をしています。Spark MLlibでいうStringIndexer相当の処理をHiveQLクエリで表現しています。

*6:ここで、train_orderedでquantify関数への入力を一定のルールで順序付けることで採番方法を一貫性のあるものとしております。なお、事前にカテゴリカル変数のカラムについてはstring型等に変更するか、quantify関数への入力時にCAST(xxx as string)で文字列型に変更して頂く必要があります。

*7:交差検定によりsalesの値を目的変数としてそのまま使うかlogを取るかを判断しました。目的変数をlog scaleに変換したほうが精度が良かったため、logスケールを利用しています。

*8:aなものが存在しないときなどに起こり得る。

*9:Hadoopクラスタのリソース状況によっては5未満のタスクが同時に走ることもあります。

*10:なお、デフォルトでは全て量的変数として変数が扱われます。Scikit-learnでも全て量的変数として決定木が構築されますが、十分な深さの木を構築すれば量的変数として扱っても経験的に問題なく動きます。

*11:Treasure DataのテーブルをPandasのDataframeに双方向変換出来ますので、ぜひご活用ください。

*12:国内のコンビニとかでも同じでしょうか。

*13:Scikit-learnのRandomForest実装でPublic LB: 0.12490, Private LB: 0.13529でしたので概ね同様の結果が得られました。

*14:Hivemallでは"-seed"オプションを指定することでDetermisticな挙動をとることも可能です。