擬似ログデータ作成ツール「logficition」を作りました

## この記事について
この記事では新しく作った[logfiction](https://github.com/rilmayer/logfiction)と言うrubygemの紹介をします。
よろしくお願いします。

## 課題感
データサイエンティストにとって「Webサイトのログデータを分析する経験」は非常に重要です。
しかし、Webサイトのログというのはそれぞれの企業が独自に所持しているため、なかなか扱うことができず、分析の経験を積む上で一つのハードルではないかと考えています。
私自身インターン等で複数企業での経験があるからこそ、データ分析をある程度できるようになりましたが、駆け出しのデータサイエンティストにとって数社のデータ分析に携わる機会はあまりありません。

## 解決策
そこで、「企業には所属していないけれどもWebサイトのログデータを分析する練習ができるようにする」ために、誰でも手元でそれっぽいログデータを生成できるツールを作って配布するのが良いのではないかと思いました。

具体的にはRubyの入ったPCで以下のコマンドを入力するだけで、架空のECサイト上の1,000行の行動ログデータが生成できます。（実行時のカレントディレクトリにlogfiction.csvが生成されます。）

```shell
# gemのインストール
$gem install logfiction

# 以下のコマンド実行により実行時のフォルダに「fictionlog.csv」という名前の擬似ログファイルが出力される（デフォルトでは1000行）
$logfiction

# 以下のようなオプションが指定できる
#   50000行の擬似ログを "./log.csv" に出力
$logfiction 50000 ./log.csv
```
このコマンドにより生成されるログは以下のような感じとなります。
<img width="863" alt="スクリーンショット 2019-04-22 2.19.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108223/a4c03011-ae73-cb41-a1f2-0e0aa101668a.png">


## 用途
大量のログデータを作って分析の練習をしたり、擬似的なWebサイトを作ってユーザーの動きをシミュレーションしてみたりできるかと思います。
個人的にはデータサイエンス系の授業で課題とかに使えるようになると良いなと思ってます。

## logfiction
ここからは擬似ログデータ生成ツールであるlogfictionの説明をします。

### logfictionの概要
logfictionはユーザーのサイト内行動をランダムウォークに見立ててユーザーの行動をシミュレーションすることでログデータを生成しています。
以下では、logfictionでどのようにのWebサイト上のユーザー行動ををシュミレートしているかを説明していきます。

logfictionでは以下の4つの重要な概念があります。

- 状態（state）
- 状態遷移（taranstion）
- アイテム（item）
- ユーザー（user）

それぞれ、説明していきます。

### 状態（Webページ上のユーザー行動）
今、仮に以下のようなECサイトを模したWebページがあったとします。

![fiction EC site page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108223/2b7ad3c4-db3a-9d83-4467-791c1d1433eb.png)

上記例のようなWebサイトでは、3つのページと1つのアクション、Webサイトの閲覧を終了するという「離脱（Exit）」という状態も含めるとユーザーの取りうる状態は以下の5つとなります。

- TOPページ閲覧（top_page_view）
- 検索結果一覧ページ閲覧（list_page_view）
- 詳細情報ページ閲覧（detail_page_view）
- アイテム購入（item_purchase）
- 離脱（exit）

logfictionでは上記のように、ユーザーの取りうる状態（state）を列挙し、その状態間の移動をシミュレートすることでログを生成します。

### ユーザーの状態間遷移とファネルモデル
次はユーザーが状態間をどのように遷移するかということを考えていきます。

logfictionでは状態遷移の考え方の基本にファネルモデルを用います。
ファネルモデルとはユーザーの人数が「購入」に近づけば近づくほどその数が少なくなっていく様子をファネル（漏斗）に見立てて図式化したモデルです。
上記例のウェブサイトでは以下のようなファネルモデルを作ることができます。（遷移率はテキトウに設定しました。）

![fiction EC site-Page-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108223/17836200-0608-7aa0-ae68-20e63ef27ca2.png)


これを示すと以下のようになります。

- top_page_viewからlist_page_viewへの遷移は60%
- list_page_viewからdetail_page_viewへの遷移は40%
- detail_page_viewからitem_purchasejへの遷移は20%

logfictionでは上記のファネルモデルに沿った、降下方向の遷移確率のみを設定すれば他の遷移については自動で設定されるようになっています。
logfictionではファネル降下しないユーザーには「離脱」と「1つ前の状態（ブラウザの戻るボタン）」の2つの状態遷移の可能性があると仮定して、2つの遷移確率はデフォルトで3:7と仮定しています。
なお、ファネルの最上位の状態のユーザーに関しては降下以外の移動先は「離脱」のみとなります。

上記の仮定に沿ったユーザーの行動遷移は以下のようなグラフで表現できます。

![state_transiton.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108223/03cd5888-beed-ce8a-5aa2-f174ae49d8a0.png)


上記の図ではノードがユーザーの状態を表し、エッジ上の数値が遷移確率を表しています。
なお、上記のグラフでは「購入後は自動でTOPページに遷移させる」（auto_transiton）、「ユーザーはトップページの閲覧からスタートする（start）」という2つの仮定を置いています。（通常のWebサイトでは購入といったアクションはその後のページ遷移が自動で設定されているため）

ここまでの内容はlogfictionでは以下のように記述します。

```rust
require "logfiction"

la = Logfiction::AccessLog.new()

states = [
    {state_id: 0, state_name: "top_page_view"},
    {state_id: 1, state_name: "list_page_view"},
    {state_id: 2, state_name: "detail_page_view"},
    {state_id: 3, state_name: "item_purchase"}
]

transitons = [
    {from: 0, to: 1, probability: 0.6},
    {from: 1, to: 2, probability: 0.4},
    {from: 2, to: 3, probability: 0.2, auto_transiton: 0}
]

states_transtions = {
    start_state: [0] # 遷移開始の状態ID
    states: states,
    transitons: transitons
}

la.generate_state_transiton(states_transtions)
```

### アイテム
logfictionでは「どのアイテムを閲覧して購入したか」という擬似データも生成することができます。
Webサイトで生成されるページについては、例えば「検索結果一覧ページ」でもそこに表示されているアイテムによって何パターンも考えられます。

logfictionでは、デフォルトでアイテムIDが100個用意されます。（この値はオプションで設定することができます。）例えば以下のような形でアイテム列が用意されます。

```
[0, 1, 2, 3, ...]
```

#### 状態とアイテム
logfictionでは状態ごとに以下の3つのアイテムタイプが定義されています。

- no_item … ページ内でアイテムなし（トップページ閲覧など）
- many … ページ内に複数アイテムが存在している（一覧ページ閲覧など）
- one … ページ内に1つのアイテムが存在している（詳細ページ閲覧、購入など）

状態のアイテムタイプが`items`か`item`の場合、状態に対して1つ以上のアイテムが紐づいている状態となります。
「Items」の場合、アイテム群から生成された有限のアイテム列が状態に紐付きます。
「item」の場合はアイテム群のうちの1つの特定アイテムが状態に紐付きます。

#### 遷移とアイテム
logfictionでは上記で設定した状態のアイテムタイプの他に、状態遷移についてのアイテム依存も定義されます。状態遷移のアイテム依存は以下の2つです。

- 依存なし … 遷移間でアイテムの考慮は不必要
- 依存あり … 遷移元と遷移先のアイテムタイプによって以下のように挙動が異なる

依存ありの場合は以下のように場合によって挙動が異なります。以下のパターンに含まれていないものについては考慮漏れか、エラーとなります。

|遷移元|遷移先|挙動|
|-----|-----|---|
|no_item |many| 生成されたアイテム列が遷移先の状態に紐付けられる。デフォルトのアイテム列の生成方法については、事前に用意されたアイテム群から10個ランダムに選択した「アイテム列群」の中からランダムで1つが選択される。（[TODO] 非ランダム）|
|many|one|遷移元のアイテム列の中から1つのアイテムがランダムに選ばれ、遷移先の状態に紐付けられる（[TODO]非ランダム）|
|one|many|遷移元のアイテムを含むように遷移先のアイテムリストが作られる|
|one|one|遷移元のアイテムが遷移先のアイテムに紐付けられる|


ここまでの内容を含めるとlogfictionは以下のように設定できます。

```ruby
require 'logfiction'

la = Logfiction::AccessLog.new()

states = [
    {state_id: 0, state_name: 'top_page_view', item_type: :no_item},
    {state_id: 1, state_name: 'list_page_view', item_type: :many},
    {state_id: 2, state_name: 'detail_page_view', item_type: :one},
    {state_id: 3, state_name: 'item_purchase', item_type: :one}
]

transitons = [
    {from: 0, to: 1, probability: 0.6, restriction: false},
    {from: 1, to: 2, probability: 0.4, restriction: true},
    {from: 2, to: 3, probability: 0.2, restriction: true, auto_transiton: 0}
]

states_transtions = {
    start_state: [0]
    states: states,
    transitons: transitons
}

la.generate_state_transiton(states_transtions)
```

この設定がlogfictionの基本的な設定となります。
そして、この設定ファイルをグラフとして可視化すると、以下のような遷移図となります。

![state_transiton.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108223/f6168ea6-085c-abad-6345-8ba16895d7d1.png)



### ユーザー
logfictionはユーザーがWebサイト上の状態を遷移する過程をシミュレーションすることでログが生成されます。
デフォルトでは100人のユーザーが生成されますが、以下のような設定を行いユーザーの行動を変化させることもできます。
ユーザーの行動制限として設定できる項目は以下の2つです。

- セッション上限
- アクション回数上限

logfictionでは30分以内で連続した行動をそのユーザーのセッションと定義しています。
ユーザーはアクション回数上限（デフォルトは100）の上限に達するまでサイト内をランダムウォークします。一度離脱したユーザーは30分後にサイトに戻ってきますが、この動作が許されるのはセッション上限（デフォルトは5）までです。
なお、この値は以下のようにいじることができます。

```ruby
require 'logfiction'

# セッション上限を10、アクション上限を200に設定
assumtions = {
    user_max_sessions: 10,
    user_max_actions: 200,
}

la = Logfiction::AccessLog.new(assumtions)
```


### その他のオプション
その他、以下のようなオプションを設定することができます。（デフォルト値が設定されているため、設定しなくても楽しめます。）

- ログの開始時刻（デフォルトは私の誕生日になってます）
- ユーザー数
- アイテム数

こうしたオプションはlogficitonでインスタンスを作成する際に、ハッシュに入れて引数として渡してあげると設定ができます。以下のような感じです。

```ruby
assumtions = {
    # ログの開始時刻を2018年01月01日 AM9:00に設定
    time_access_from: Time.parse("2018-01-01 09:00:00"),
    
    # セッション上限を20に設定
    user_max_sessions: 20,

    # アクション上限を200に設定
    user_max_actions: 100,

    # 生成するユーザー数を500に設定
    n_users: 500,

    # 生成するアイテム数を200に設定
    n_items: 200
}

la = Logfiction::AccessLog.new(assumtions)

```

### ログの取得
上記のようにWebサイトの構造を作ったり、いくつかの仮定を置いたら実際にログの生成を行うことができます。（デフォルトで設定されているので、必ずしも設定する必要はありません）

```ruby

# 仮定や状態遷移の設定
la = Logfiction::AccessLog.new(assumtions)
la.generate_state_transiton(states_transtions)

# ログの取得
logs = la.generate_accesslog(100)
# => [
#     {:timestamp=>2018-01-01 09:01:48 +0900, :user_id=>0, :state_id=>0, :items=>"", :state_name=>"top_page_view"},
#     {:timestamp=>2018-01-01 09:02:59 +0900, :user_id=>0, :state_id=>1, :items=>"80:81:82:83:84:85:86:87:88:89", :state_name=>"list_page_view"},
#     {:timestamp=>2018-01-01 09:04:43 +0900, :user_id=>0, :state_id=>2, :items=>"80", :state_name=>"detail_page_view"},
#     {:timestamp=>2018-01-01 09:36:22 +0900, :user_id=>0, :state_id=>0, :items=>"", :state_name=>"top_page_view"},
#     ...

# ログの出力
la.export_logfile(100, filetype="CSV",filepath="output.csv")
```

## 今後追加予定の機能
- ユーザーごとに好みを設定してアイテムの選択傾向を出せるようにする
- 様々なWebサイトの構造をデフォルトで追加
- 状態に対してURLを紐付けてNginxのようなリアルなログを生成する
- などなど

## 終わりに
まだ全然磨き込めていないので、これから良いライブラリにしていこうと思います！
フィードバック等いただけるととても嬉しいです。