juliaで前処理大全 3.集約
前処理大全の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
最頻値の算出
: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に用意されています。
関数名 | 表示法 |
---|---|
ordinalrank | 1234 |
competerank | 1223 |
denserank | 1223 |
tiedrank | 1 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
つづく