juliaで前処理大全 3.集約

(src=https://pixabay.com/photos/food-salad-raw-carrots-1209503/)

前処理大全のjulia版その2です。今回のテーマは集約です。特定の列のデータ毎にデータフレームをグループ分けして、 グループ化されたデータフレーム毎に処理を行い、最終的に集約します。

集約

準備としてホテルの予約データを読み込みます。

using DataFrames,CSV,Chain,Downloads
reserve_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/reserve.csv"
reserve_df = @chain reserve_url begin
  Downloads.download(IOBuffer())
  String(take!(_))
  CSV.read(IOBuffer(_),DataFrame)
end
println(first(reserve_df));
DataFrameRow
 Row │ reserve_id  hotel_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price
     │ String7     String7   String7      String31             Dates.Date    Dates.Time    Dates.Date     Int64       Int64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ r1          h_75      c_1          2016-03-06 13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200

データ数、種類数の算出

ホテルごとに予約数、顧客数の集計を行う練習です。 ホテルごとにデータを分ける処理は、groupby関数で実施します。

groupby関数は、groupby(df::AbstractDataFrame,cols;kwargs...) のようにデータフレームと列名を引数にもちます。 返り値の型はGroupedDataFrameとなります。

グループ化したデータ毎に集計する処理はcombine関数で実施します。 combine関数は、GroupedDataFrameを引数にして、さらに 列名 => 関数 (=> 新しい列名)のような書き方の可変長引数をとることができます。

この問題は、@chainマクロを用いたパイプライン処理によって、以下のように、簡潔に記述できます。 一行目のデータを確認してみましょう。

using Chain
df3_1= @chain reserve_df begin
  groupby(:hotel_id)
  combine(:reserve_id  => length        => :rsv_cnt,
          :customer_id => length∘unique => :cus_cnt)
end
first(df3_1) |> println
DataFrameRow
 Row │ hotel_id  rsv_cnt  cus_cnt
     │ String7   Int64    Int64
─────┼────────────────────────────
   1 │ h_75           12       12

予約数は、length関数で集計可能です。 顧客数を集計するには、顧客名の重複を消すために、length∘uniqueのように unique関数とlength関数の合成関数を利用しました。

unique関数とlength関数を組み合わせる書き方は前処理大全では推奨されていない書き方ですが、 この例では可読性が十分高く、処理速度もおそらく問題にならないでしょうからAwesomeということにします。

合計値の算出

ホテルと宿泊人数毎に集計してみます。複数の列についてグループ分けして集計を行う場合は、列名をArrayとして、 groupby関数の引数にすればOKです。 また、グループごとに列の合計値を出力するには、sum関数を指定すればよいです。

df3_2 = @chain reserve_df begin
  groupby([:hotel_id,:people_num])
  combine(:total_price=> sum =>:price_sum)
end
first(df3_2) |> println
DataFrameRow
 Row │ hotel_id  people_num  price_sum
     │ String7   Int64       Int64
─────┼─────────────────────────────────
   1 │ h_75               1     113400

極値、代表値の算出

ホテル毎にtotal_price列の最大値、最小値、平均値、中央値、20%のパーセンタイルを出力します。 平均値、中央値、パーセンタイルの出力には、新たな関数が必要なので、Statistics.jlを導入します。 パーセンタイルは、無名関数を使用するため、やや冗長な書き方となっています。

using Statistics
@chain reserve_df begin
  groupby(:hotel_id)
  combine(:total_price=>maximum=>:price_max,
          :total_price=>minimum=>:price_min,
          :total_price=>mean=>:price_mean, 
          :total_price=>median=>:price_medeian,
          :total_price=>(r->quantile(r,0.2))=>:price_20per) 
  first(10)
  println
end
10×6 DataFrame
 Row │ hotel_id  price_max  price_min  price_mean      price_medeian  price_20per
     │ String7   Int64      Int64      Float64         Float64        Float64
─────┼────────────────────────────────────────────────────────────────────────────
   1 │ h_75          97200      16200   49950.0              36450.0      24300.0
   2 │ h_219         92700      10300   46746.2              30900.0      20600.0
   3 │ h_179         67200       5600   32421.1              33600.0      17920.0
   4 │ h_214        437400     145800       2.51836e5       291600.0     194400.0
   5 │ h_16         272400      45400       1.42174e5       136200.0      68100.0
   6 │ h_241         72000       6000   29555.6              24000.0      12000.0
   7 │ h_256        414000      34500       1.53682e5       103500.0      69000.0
   8 │ h_217        136800      22800   78763.6              68400.0      45600.0
   9 │ h_240        320400      26700  137060.0              80100.0      48060.0
  10 │ h_183        118800       9900   48400.0              39600.0      29700.0

ばらつき具合の算出

分散や標準偏差は、平均同様にStatistics.jlをusingすれば使えます。 分散や標準偏差は、データ数が1つしかない場合にはNaN(Not a Number)を返します。

using Statistics
@show var([1,5,9]) # 分散
@show std([1,5,9]) # 標準偏差
@show var([1])
@show std([1])
var([1, 5, 9]) = 16.0
std([1, 5, 9]) = 4.0
var([1]) = NaN
std([1]) = NaN

varとstdについてNaNを0に変更する方法を調査しました。いくつか方法はありましたが、 ifelse(isnan(x),0,x)という関数を使うのが一番短く書けそうです。 (R版の回答にあるcoalesce関数はjuliaにもありますが、機能が異なるようです。)

NaNを0に置き換える処理はパイプライン処理の中で無名関数を使って実装することもできますが、 可読性は低いので、新しく関数を作るか、ブロードキャスト演算で最後にまとめて処理するのがAwesomeでしょう。

using Chain,Statistics,DataFrames
df3_4=@chain DataFrame(a=["あ","あ","い"],b=[1,2,3]) begin
  groupby(:a)
  combine(:b=>sum,
          :b=>(r->ifelse(isnan(var(r)),0,var(r))) =>:b_var,
          :b=>std)
end
@show df3_4
df = df3_4[!,2:end] #2列目以降のviewを作成。1列目にisnanを適用するとエラーがかえって来ます。
df .= ifelse.(isnan.(df),0,df) #ブロードキャスト演算
println()
@show df3_4
df3_4 = 2×4 DataFrame
 Row │ a       b_sum  b_var    b_std
     │ String  Int64  Float64  Float64
─────┼────────────────────────────────────
   1 │ あ          3      0.5    0.707107
   2 │ い          3      0.0  NaN

df3_4 = 2×4 DataFrame
 Row │ a       b_sum  b_var    b_std
     │ String  Int64  Float64  Float64
─────┼──────────────────────────────────
   1 │ あ          3      0.5  0.707107
   2 │ い          3      0.0  0.0

この方法を例題に適用すると以下のようになります。

df3_4=@chain reserve_df begin 
  groupby(:hotel_id)
  combine(:total_price=>var,
          :total_price=>std)
end
df = df3_4[!,2:end] #2列目以降のviewを作成。1列目にisnanを適用するとエラーがかえって来ます。
df .= ifelse.(isnan.(df),0,df) #ブロードキャスト演算
@show first(df3_4,10)
first(df3_4, 10) = 10×3 DataFrame
 Row │ hotel_id  total_price_var  total_price_std
     │ String7   Float64          Float64
─────┼────────────────────────────────────────────
   1 │ h_75           1.09549e9     33098.2
   2 │ h_219          8.59601e8     29319.0
   3 │ h_179          3.08648e8     17568.4
   4 │ h_214          7.47238e9     86442.9
   5 │ h_16           6.57522e9     81087.7
   6 │ h_241          4.53949e8     21306.1
   7 │ h_256          1.77023e10        1.3305e5
   8 │ h_217          1.46737e9     38306.2
   9 │ h_240          1.16982e10        1.08158e5
  10 │ h_183          7.94329e8     28183.9
😲 Note
前処理大全の著者によれば、データはパートナーの心と同様に不安定でばらつきがあるものだそうです。 分散値/標準偏差値とパートナーの気持ちを常々気にかけたいと思います。

最頻値の算出

:total_priceを100の位を四捨五入して、最頻値を求めます。 四捨五入はround関数で実行できますが、丸める桁はキーワード引数digitsで指定する必要があります。 最頻値については、StatsBase.jlをインポートしておけば、pythonと同じくmode関数で求めることが可能です。

using StatsBase
mode(round.(reserve_df.total_price, digits=-3)) |> println
20000.0

2万円くらいホテルにお金を落としている人が一番多いということですね。

順位の算出

ホテルごとに予約日時順の番号付けをしてみます。 そもそも予約日時がString型として認識されているので、まずはDateTime型にパースします。

using Dates
reserve_df.reserve_datetime = DateTime.(reserve_df.reserve_datetime,dateformat"y-m-d HH:MM:SS")
reserve_df |> first |> println
DataFrameRow
 Row │ reserve_id  hotel_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price
     │ String7     String7   String7      Dates.DateTime       Dates.Date    Dates.Time    Dates.Date     Int64       Int64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ r1          h_75      c_1          2016-03-06T13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200

ランキング付けに対応する関数はStatsBase.jlに用意されています。

関数名表示法
ordinalrank1234
competerank1223
denserank1223
tiedrank1 2.5 2.5 4

ランキング方法にもいくつか種類がありますが、日時が完全に一致することはないでしょうから、 ここではordinalrankを利用します。

前処理の手順としては、groupby関数でhotel_id毎にデータを分けて、select関数で必要な列を選びます。

select関数の引数は:列名 => 処理 => :新しい列名というようにcombine関数と同じような与え方が可能です。 ではやってみます。

using StatsBase
@chain reserve_df begin
  groupby(:hotel_id)
  select(:,:reserve_datetime => ordinalrank =>:log_no)
  first(10)
  println
end

10×10 DataFrame
 Row │ hotel_id  reserve_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price  log_no
     │ String7   String7     String7      Dates.DateTime       Dates.Date    Dates.Time    Dates.Date     Int64       Int64        Int64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ h_75      r1          c_1          2016-03-06T13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200       3
   2 │ h_219     r2          c_1          2016-07-16T23:39:55  2016-07-20    11:30:00      2016-07-21              2        20600       4
   3 │ h_179     r3          c_1          2016-09-24T10:03:17  2016-10-19    09:00:00      2016-10-22              2        33600      12
   4 │ h_214     r4          c_1          2017-03-08T03:20:10  2017-03-29    11:00:00      2017-03-30              4       194400       9
   5 │ h_16      r5          c_1          2017-09-05T19:50:37  2017-09-22    10:30:00      2017-09-23              3        68100      18
   6 │ h_241     r6          c_1          2017-11-27T18:47:05  2017-12-04    12:00:00      2017-12-06              3        36000      25
   7 │ h_256     r7          c_1          2017-12-29T10:38:36  2018-01-25    10:30:00      2018-01-28              1       103500      11
   8 │ h_241     r8          c_1          2018-05-26T08:42:51  2018-06-08    10:00:00      2018-06-09              1         6000      26
   9 │ h_217     r9          c_2          2016-03-05T13:31:06  2016-03-25    09:30:00      2016-03-27              3        68400       2
  10 │ h_240     r10         c_2          2016-06-25T09:12:22  2016-07-14    11:00:00      2016-07-17              4       320400       4
一番右の列に:log_noの列が追加されていることが確認できます。

Q ランキング

ホテル毎に予約数を集計して、ランキング付けを行います。同じ予約数の場合は、 最小の順位を付けることになっているので、ordinalrankではなくて、competerankを利用します。

@chain reserve_df begin
  groupby(:hotel_id)
  combine(:reserve_id => length => :rsv_cnt)
  select(:,:rsv_cnt => competerank =>:rsv_cnt_rank)
  first(10)
  println
end
10×3 DataFrame
 Row │ hotel_id  rsv_cnt  rsv_cnt_rank
     │ String7   Int64    Int64
─────┼─────────────────────────────────
   1 │ h_75           12           100
   2 │ h_219          13           127
   3 │ h_179          19           270
   4 │ h_214          11            67
   5 │ h_16           19           270
   6 │ h_241          27           300
   7 │ h_256          11            67
   8 │ h_217          11            67
   9 │ h_240          15           187
  10 │ h_183          18           259

つづく