CSV.read
関数は、出力するデータ型(この場合は DataFrame 型)を指定しないとエラーを吐くようになりました。juliaで前処理大全 2.抽出
juliaで前処理大全をやります。今回は、データの読みこみと抽出のやり方を見ていきます。 内容は前処理大全の第2章に相当します。
@chainマクロを使ったパイプライン処理についても簡単に解説します。
ファイルを読み込む
CSV ファイルの読み込みでは、以下の方法をよく使います。
using DataFrames,CSV
df = CSV.read("file.csv", DataFrame)
CSVファイル以外の読み込みでは、Excelから読み込むことが多いので、 ExcelFiles.jlをよく利用します。こちらはシート名を直接指定する必要があります。
using DataFrames, ExcelFiles
df = DataFrame(load("file.xlsx","Sheet1"))
datファイルなら、標準パッケージのDelimitedFiles.jlを利用すると読み込みが高速です。
using DataFrames,DelimitedFiles
df = DataFrame(readdlm("file.dat"))
そのほかに、Rとpythonで共通なFeather形式を利用していたこともありましたが、 ほかの言語で処理することもなかったし、特段ファイルサイズが小さくなるわけでもありませんでした。
さて、早速サンプルのホテルの予約データ(reserve.csv)を読み込んでいきます。 一行目のデータを表示してみましょう。
using DataFrames,CSV,Chain,Downloads
reserve_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/reserve.csv"
reserve_df = @chain reserve_url Downloads.download CSV.File DataFrame
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
いきなり@chainマクロを使ったパイプライン処理がでてきますが、これは次のセクションで解説します。
パイプライン処理
julia標準のパイプライン処理
「そもそもパイプライン処理とは何ぞや?」から解説したいと思います。 簡単な例として、次のような関数が入れ子になった計算をしたい場合を考えます。
sin(cos(tan(1)))
関数名が短い場合は良いのですが、このような入れ子になった式は、一般的に可読性が高いとは言えません。 そこで、作用させる関数を後から追加できるように、juliaでは次のパイプ演算子|>
が用意されています。 フォントの関係で右三角に見えますが、実際の入力は|>です。
1 |> tan |> cos |> sin
このような記法を採用するメリットは、可読性が高まるだけでなく、実行結果を確認しながら後で関数を追加できることです。
引数が複数ある場合の対応
ところが、複数の引数をとる関数をパイプライン処理で実装したいときは、 そのままでは実行できない問題が発生します。そこで、無名関数を使う必要がでてきます。
add(a,b) = a+b
1 |> (x -> add(x,2))
ただし、このような書き方をすると可読性が犠牲になります。
この問題を解決できるのがパイプライン処理のパッケージです。
Chain.jl パッケージ
パイプライン処理のパッケージはいくつかありますが、ここではChain.jlを使うことにします。 使い方を簡単に解説します。
@chainマクロを使うと、そもそもパイプ演算子|>
を記述する必要がなくなります。 最初の例は、次のように一行で書けます。
using Chain
@chain 1 tan cos sin
2番目の例のように、複数の引数をとる関数がきても大丈夫です。
add(a,b) = a+b
@chain 1 add(2) add(3)
6
左側の計算結果を、次の関数の最初に引数として扱ってくれるからです。 この@chainマクロが何をしているか展開して確認してみると
using Chain
@macroexpand @chain 1 add(2) add(3) add(4)
quote
local var"##444" = 1
local var"##445" = add(var"##444", 2)
local var"##446" = add(var"##445", 3)
local var"##447" = add(var"##446", 4)
var"##447"
end
複数行で実行
複数行で実行したい場合は、begin-end
環境で囲います。 最初の例でやってみましょう。
@chain 1 begin
tan
cos
sin
end
0.013387802193205699
引数の位置を明示
引数の位置を明示したい場合は、_
(アンダースコア)を用います。 これは、ほかのパイプライン処理のパッケージでも同じ実装であることが多いです。
マクロを展開すると_
の位置がlocal変数で書き換えられていることがわかると思います。
@macroexpand(@chain reserve_url begin
Downloads.download(IOBuffer())
String(take!(_))
CSV.read(IOBuffer(_),DataFrame)
end) |>Base.remove_linenums!
quote
local var"##452" = reserve_url
local var"##453" = Downloads.download(var"##452", IOBuffer())
local var"##454" = String(take!(var"##453"))
local var"##455" = CSV.read(IOBuffer(var"##454"), DataFrame)
var"##455"
end
@asideマクロ
パイプライン処理の途中の結果で何かをしたいときには、@asideマクロが使えます。 そのうち使うかもしれません。
@chain 1 begin
tan
@aside println(_)
cos
sin
end
1.5574077246549023
0.013387802193205699
@chainマクロとDataFrames.jlは非常に相性が良いので、この先ガンガン使っていきます。
抽出
データ列の抽出
前処理大全では、可読性を高めるために、データの列番号ではなく、ラベルによって列を抽出することが推奨されています。 まず、ラベル名を確認しておきます。
@show names(reserve_df);
names(reserve_df) = ["reserve_id", "hotel_id", "customer_id", "reserve_datetime", "checkin_date", "checkin_time", "checkout_date", "people_num", "total_price"]
試しにreserve_id
とhotel_id
の列を抽出してみます。
String で指定
オーソドックスなのは、ラベル名をString型で指定する場合です。
reserve_df[!,["reserve_id","hotel_id"]] |> first |> println
DataFrameRow
Row │ reserve_id hotel_id
│ String7 String7
─────┼──────────────────────
1 │ r1 h_75
Symbol で指定
同じことを Symbol型で指定してやってみます。
reserve_df[!,[:reserve_id,Symbol("hotel_id")]] |> first |> println
println()
@show typeof(:reserve_id)
DataFrameRow
Row │ reserve_id hotel_id
│ String7 String7
─────┼──────────────────────
1 │ r1 h_75
typeof(:reserve_id) = Symbol
:reserve_id
とSymbol("reserve_id")
は同じ結果を示します。 ともに型はSymbol型です。
:reserve_id
の方が、タイプ数が少ないので省エネです。
また、一列だけ抽出したいときは、df.reserve_id
のようにすることもできます。 ただし、返り値の型はDataFrame型ではなく、Vecter型になります。
@show reserve_df.reserve_id[1:5]
println()
@show typeof(reserve_df.reserve_id)
reserve_df.reserve_id[1:5] = InlineStrings.String7["r1", "r2", "r3", "r4", "r5"]
typeof(reserve_df.reserve_id) = Vector{InlineStrings.String7}
いつでも:reserve_id
のようにできるわけではありません。よくあるトラップが、 ラベル名に%
や/
などの2項演算子が文字列に使われている時です。
testdf = DataFrame("a%"=>"a", "b(m/s)"=>1:1e5)
println(first(testdf))
DataFrameRow
Row │ a% b(m/s)
│ String Float64
─────┼─────────────────
1 │ a 1.0
この場合には、df[!,Symbol("a%")]
のようにして指定する必要があります。 演算子がラベル名に使われている場合は、私は最初にラベル名を当たり障りのないように変更するようにしています。
reserve_df[:,["reserve_id"]]
でもreserve_df[!,["reserve_id"]]
でも、返り値は同じです。 ただし、前者の場合は新しくメモリ割り当てが発生します。その結果として実行速度も遅くなります。
@time testdf[:,["a%","b(m/s)"]]
println()
@time testdf[!,["a%","b(m/s)"]]
0.006153 seconds (4.72 k allocations: 1.858 MiB, 93.06% compilation time)
0.000017 seconds (18 allocations: 1.391 KiB)
select 関数
列を抽出するには、select 関数も利用できます。
select(reserve_df,:reserve_id, :hotel_id) |> first |> println
DataFrameRow
Row │ reserve_id hotel_id
│ String7 String7
─────┼──────────────────────
1 │ r1 h_75
select関数には、新しい列を作る機能もあるので、 後で活躍しそうです。
Not や正規表現による抽出
ラベルがid
で終わる列を正規表現で抽出します。
reserve_df[!,r"id$"] |> first |> println
DataFrameRow
Row │ reserve_id hotel_id customer_id
│ String7 String7 String7
─────┼───────────────────────────────────
1 │ r1 h_75 c_1
ラベル名がid
で終わらない列を抽出するには、Not に入れてしまうと楽です。 Not の中身は、直接ラベル名を指定することもできるので有用です。
reserve_df[!,Not(r"id$")] |> first |> println
DataFrameRow
Row │ reserve_datetime checkin_date checkin_time checkout_date people_num total_price
│ String31 Dates.Date Dates.Time Dates.Date Int64 Int64
─────┼─────────────────────────────────────────────────────────────────────────────────────────
1 │ 2016-03-06 13:09:42 2016-03-26 10:00:00 2016-03-29 4 97200
条件指定による抽出
checkin_date
が2016-10-12から2016-10-13までのデータを抽出します。 ここではfilter関数を使います。 filter関数には、見ての通り2通りの使い方があります。 基本的には無名関数を使って、引数x-> a<x<b
のような書き方をしています。
using Dates: Date
# filter(:checkin_date => row-> Date(2016,10,12) <= row <= Date(2016,10,13), reserve_df) |> println
filter(row-> Date(2016,10,12) <= row.checkin_date <= Date(2016,10,13), reserve_df) |> println
8×9 DataFrame
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 │ r285 h_121 c_67 2016-09-27 06:13:19 2016-10-12 12:00:00 2016-10-14 4 184000
2 │ r1933 h_113 c_477 2016-09-24 09:04:26 2016-10-12 11:30:00 2016-10-13 4 77200
3 │ r2291 h_230 c_574 2016-10-09 04:34:14 2016-10-12 12:00:00 2016-10-13 1 17400
4 │ r2524 h_203 c_631 2016-09-14 10:45:15 2016-10-12 10:30:00 2016-10-14 3 167400
5 │ r3147 h_163 c_794 2016-10-02 07:35:16 2016-10-13 09:00:00 2016-10-16 1 64200
6 │ r3328 h_23 c_833 2016-09-28 08:22:57 2016-10-13 09:00:00 2016-10-14 4 260400
7 │ r3381 h_110 c_844 2016-09-17 17:44:02 2016-10-13 12:30:00 2016-10-15 1 52800
8 │ r3444 h_14 c_859 2016-10-03 17:26:00 2016-10-13 12:30:00 2016-10-15 3 46200
上の例では1行で書きましたが、いくつかの前処理を一気に行うなら、以下のように複数行に分けたほうが、可読性は高いでしょう。
using Chain
@chain reserve_df begin
filter(:checkin_date => >=(Date(2016,10,12)),_)
filter(:checkin_date => <=(Date(2016,10,13)),_)
# println
end
データ値に基づかないサンプリング
データ数を大体半分にしたいときの処方箋です。 pythonやRのようにDataFrame用にsample関数のようなものがあってもよさそうですが、 ないみたいなので、StatsBase.jlのsample関数を利用します。 DataFrameの行数から重複を許さずに、半分の値をサンプリングします。 そして、それを行番号として指定します。
using StatsBase: sample
sample_row = sample(1:nrow(reserve_df), round(Int,nrow(reserve_df)/2), replace=false)
reserve_df[sample_row,:]
SQLで出てきた、乱数を使って、データ数を約半分にする方法も使えそうです。
using Chain
reserve_df.rand=rand(nrow(reserve_df))
sdf = @chain reserve_df begin
filter(:rand =><(0.5),_)
select!(Not(:rand))
end
println(first(sdf))
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 │ r4 h_214 c_1 2017-03-08 03:20:10 2017-03-29 11:00:00 2017-03-30 4 194400
2-4 集約IDに基づくサンプリング
顧客IDを集約して、半分にサンプリングし、顧客IDによるデータの抽出を行います。 IDの重複をなくすにはunique関数が便利です。
@chain reserve_df.customer_id begin
unique
sample(_, round(Int,length(_)/2),replace=false)
filter(:customer_id => in(_),reserve_df)
end
unique関数はデータフレーム全体に適用することも可能です。
DataFrame(a=[1,1,2],b=["a","a","b"]) |> unique
2×2 DataFrame
Row │ a b
│ Int64 String
─────┼───────────────
1 │ 1 a
2 │ 2 b
つづく