lesson: 2 仮想ログデータ生成

ここでは、これから使用していくための仮想ログデータを作成します。またこのログデータ作成を通じて、lubridateのperiodpオブジェクトについて紹介します。

2.1 想定シナリオ

準備するログデータは以下のようなシナリオとします:

  • あるサービスのアイテム購入履歴ログデータ
  • 2018年1月1日から50日間のデータ
  • 購入は24時間常に発生
    • 発生確率は(めんどうなので)一様とする
  • アイテム名はitem1-5の5種類
    • 価格は順に100, 500, 1000, 2000, 5000
    • 購入発生比は順に100, 50, 10, 5, 2
  • 会員IDは1000001:1000300の300名
    • 何度でも購入可能
  • ログデータの設計は以下の通り
    • stamp: タイムスタンプ
    • id: 会員ID
    • value: 購入金額
    • item: 項目ラベル
  • 今回は50日間で10000件の購入があったと仮定

2.2 データ生成

上記シナリオにあうようにデータを生成します。

2.2.1 Rコード

必要なパッケージを読み込みます。

library(tidyverse)
 #>  ─ Attaching packages ──────────────────── tidyverse 1.2.1 ─
 #>  ✔ ggplot2 2.2.1     ✔ purrr   0.2.4
 #>  ✔ tibble  1.4.2     ✔ dplyr   0.7.4
 #>  ✔ tidyr   0.8.0     ✔ stringr 1.3.0
 #>  ✔ readr   1.1.1     ✔ forcats 0.3.0
 #>  ─ Conflicts ────────────────────── tidyverse_conflicts() ─
 #>  ✖ lubridate::as.difftime() masks base::as.difftime()
 #>  ✖ lubridate::date()        masks base::date()
 #>  ✖ dplyr::filter()          masks stats::filter()
 #>  ✖ lubridate::intersect()   masks base::intersect()
 #>  ✖ dplyr::lag()             masks stats::lag()
 #>  ✖ lubridate::setdiff()     masks base::setdiff()
 #>  ✖ lubridate::union()       masks base::union()
library(lubridate)

パラメータを設定します。

start <- "2018-1-1 00:00:00" #開始日
n <- 10000 # 購入件数
duration_days <- 50 # ログの期間(日数)
list_price <- c(100, 500, 1000, 2000, 5000) # アイテムの価格リスト
list_item <- paste("item", 1:length(list_price), sep = "_") # アイテムリスト
list_item_p <- c(100, 50, 10, 5, 2) # 発生比
list_id <- 1000001:1000300 # 会員id

ログデータのdata.frameを生成します。今回はdf_logというオブジェクトとします。

df_log <- data.frame(
  # タイムスタンプを作成
  # 開始日時を生成
  stamp = ymd_hms(start) +
    # 0-50までの整数からランダムに10000件生成し、それを日数データに変換して足す
    days(sample(0:duration_days, n, replace = TRUE)) +
    # 0-23までの整数からランダムに10000件生成し、それを時間データに変換して足す
    hours(sample(0:23, n, replace = TRUE)) +
    # 0-59までの整数からランダムに10000件生成し、それを分データに変換して足す
    minutes(sample(0:59, n, replace = TRUE)) +
    # 0-59までの整数からランダムに10000件生成し、それを病データに変換して足す
    seconds(sample(0:59, n, replace = TRUE)),
  # 会員IDをランダムに生成
  id = sample(list_id, n, replace = TRUE),
  # アイテム名をランダムに生成
  item = sample(list_item, n, replace = TRUE, prob = list_item_p)
) %>%
  # ログデータっぽく、タイムスタンプで並べ替える
  arrange(stamp)

ここまでの処理で、以下のようなデータができます:

knitr::kable(sample_n(df_log, 10))
stamp id item
3954 2018-01-21 08:07:26 1000057 item_2
5733 2018-01-30 12:11:20 1000131 item_1
4285 2018-01-23 00:38:05 1000088 item_2
1881 2018-01-10 22:09:17 1000055 item_1
7720 2018-02-09 08:39:21 1000188 item_2
252 2018-01-02 07:19:54 1000090 item_1
5229 2018-01-27 22:40:43 1000110 item_2
2796 2018-01-15 11:47:17 1000042 item_1
2380 2018-01-13 09:58:16 1000243 item_1
4921 2018-01-26 06:52:28 1000086 item_1

アイテム名(item)に対応する価格(value)を当てます。

# 置換用の名前付きベクトルを作成
# 置換前文字列がnames, 置換後の文字列がベクトルの内容となるように
pat <- as.character(list_price)
names(pat) <- list_item
# itemを正規表現で置換して数値に変換し、列として追加
df_log <- df_log %>% 
  # 対応する項目を一気に置換して整数型へ変換
  # この変換方法については?stringr::str_replace_allを参照
  mutate(value = str_replace_all(item, pat) %>% 
           as.numeric())

生成したdf_logの一部を表示します:

knitr::kable(sample_n(df_log, 10))
stamp id item value
7530 2018-02-08 11:16:31 1000172 item_1 100
3646 2018-01-19 20:16:06 1000243 item_1 100
4070 2018-01-21 21:54:21 1000063 item_3 1000
3034 2018-01-16 16:16:03 1000143 item_3 1000
4262 2018-01-22 21:01:11 1000078 item_1 100
2394 2018-01-13 11:28:59 1000183 item_1 100
153 2018-01-01 19:10:26 1000083 item_1 100
9742 2018-02-19 16:02:05 1000024 item_2 500
7773 2018-02-09 15:48:54 1000008 item_1 100
8060 2018-02-11 02:55:54 1000156 item_2 500

これで仮想ログデータが完成です。一応csvに出力しときます:

readr::write_csv(df_log, path = "df_log.csv")

2.2.2 解説

ここでのポイントはタイムスタンプ(stamp)の生成です。今回は開始日時(2018-1-1 00:00:00)から50日間でランダムに生成させる必要があります。そこで利用したのがdays()などのperiodオブジェクトを生成する関数です。

periodオブジェクトは、lubridateパッケージで準備している独自のクラスで、時間的な区間です。

# 「1日分」のperiodを生成
# 引数には整数値を指定
days(1)
 #>  [1] "1d 0H 0M 0S"

# 負の整数だと「マイナス1日間」となる
days(-1)
 #>  [1] "-1d 0H 0M 0S"

# 足し引きもできる
days(3) - days(1)
 #>  Note: method with signature 'Period#ANY' chosen for function '-',
 #>   target signature 'Period#Period'.
 #>   "ANY#Period" would also be valid
 #>  [1] "2d 0H 0M 0S"
days(3) + days(-2)
 #>  [1] "1d 0H 0M 0S"

なぜこのようなことができるかというと、periodオブジェクトは時間的な区間を数値的に置き換えて保持しているからです。なので、日時の加算・減算ができるようになります:

# 日付にperiodを加算
# この場合は日付型(Date)になる
ymd("20180224") + days(3)
 #>  [1] "2018-02-27"

# 日時にperiodを加算
# この場合はPOSIXct型になる
ymd_hms("20180224 150000") + days(3)
 #>  [1] "2018-02-27 15:00:00 UTC"

また、ベクトルでの加算・減算も可能です。ベクトルの長さが揃っていない場合はリサイクルされます:

ymd("20180224") + days(1:3)
 #>  [1] "2018-02-25" "2018-02-26" "2018-02-27"

このperiodオブジェクトはdays以外にも時間(hours())やミリ秒(miliseconds())など準備してあります。これをうまく活用し、開始日時(2018-1-1 00:00:00)へランダムな日数、時間数、秒数を足し算してタイムスタンプを作成しました。

2.3 参照

Create a period object

days()などの関数の解説