juliaで前処理大全 8.数値型

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

juliaで前処理大全その7です。今回は数値型を取り扱います。

数値型への変換

Q 様々な数値型の変換

40000/340000 / 3 をさまざまな数値のデータ型に変換するというお題です。

型の確認

typeof(40000/3)
Float64

デフォルトの型を確認するとFloat64であることがわかります。

整数型へ変換

整数型に変換するにはround関数、あるいはfloor関数を用います。

round(Int, 40000/3)
13333
floor(Int, 40000/3)
13333
😲 Note

この2つの関数はとくに違いが無さそうですが、負の数に適用する場合、注意が必要です。

round(Int, -2.1),floor(Int, -2.1)
(-2, -3)

対数化による非線形な変換

log(年齢+1)=(対数化した年齢) \log (年齢 + 1) = (対数化した年齢)

のような対数変換を行うと値が大きくなるほど値の差の意味をなくす効果があります。 また、回帰分析などを行うときに目的変数が負の値をとるとまずい場合に、対数変換を適用することがあります。 この場合、対数スケールで見たときに誤差の分散が等しい仮定をすることになりますが、細かく意識している人っているのか偶に疑問を抱きます。

Q 対数化

ホテルの予約レコードのtotal_priceを1000で割って1を足して、底10で対数化するというお題です。

transform関数とByRowと無名関数を使って難なく実現できます。

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 
                    CSV.File 
                    DataFrame
                    transform(:total_price=>ByRow(c->log10(c/1000 + 1))=>:total_price_log)
                    end

first(reserve_df,10) |> println
10×10 DataFrame
 Row │ reserve_id  hotel_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price  total_price_log
     │ String7     String7   String7      String31             Dates.Date    Dates.Time    Dates.Date     Int64       Int64        Float64
─────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ r1          h_75      c_1          2016-03-06 13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200         1.99211
   2 │ r2          h_219     c_1          2016-07-16 23:39:55  2016-07-20    11:30:00      2016-07-21              2        20600         1.33445
   3 │ r3          h_179     c_1          2016-09-24 10:03:17  2016-10-19    09:00:00      2016-10-22              2        33600         1.53908
   4 │ r4          h_214     c_1          2017-03-08 03:20:10  2017-03-29    11:00:00      2017-03-30              4       194400         2.29092
   5 │ r5          h_16      c_1          2017-09-05 19:50:37  2017-09-22    10:30:00      2017-09-23              3        68100         1.83948
   6 │ r6          h_241     c_1          2017-11-27 18:47:05  2017-12-04    12:00:00      2017-12-06              3        36000         1.5682
   7 │ r7          h_256     c_1          2017-12-29 10:38:36  2018-01-25    10:30:00      2018-01-28              1       103500         2.01912
   8 │ r8          h_241     c_1          2018-05-26 08:42:51  2018-06-08    10:00:00      2018-06-09              1         6000         0.845098
   9 │ r9          h_217     c_2          2016-03-05 13:31:06  2016-03-25    09:30:00      2016-03-27              3        68400         1.84136
  10 │ r10         h_240     c_2          2016-06-25 09:12:22  2016-07-14    11:00:00      2016-07-17              4       320400         2.50705

カテゴリ化による非線形な変換

Q 数値型のカテゴリ化

ホテルの予約レコードの顧客テーブルの年齢を10刻みのカテゴリー型として追加するというお題です。 行ごとの処理と列全体を分けて行っているので、本のpythonコードなどと比べるとやや冗長な書き方になっています。

using CategoricalArrays
customer_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/customer.csv"
customer_df = @chain customer_url Downloads.download CSV.File DataFrame
transform!(customer_df, :age=>ByRow(c->10floor(Int,c/10))=>:age_rank)
transform!(customer_df, :age_rank=>categorical=>:age_rank)

first(customer_df,10) |> println
10×6 DataFrame
 Row │ customer_id  age    sex      home_latitude  home_longitude  age_rank
     │ String7      Int64  String7  Float64        Float64         Cat…
─────┼──────────────────────────────────────────────────────────────────────
   1 │ c_1             41  man            35.0922         136.512  40
   2 │ c_2             38  man            35.3251         139.411  30
   3 │ c_3             49  woman          35.1205         136.511  40
   4 │ c_4             43  man            43.0349         141.24   40
   5 │ c_5             31  man            35.1027         136.524  30
   6 │ c_6             52  man            34.4408         135.39   50
   7 │ c_7             50  man            43.0158         141.231  50
   8 │ c_8             65  woman          38.2013         140.466  60
   9 │ c_9             36  woman          33.3228         130.331  30
  10 │ c_10            34  woman          34.2904         132.303  30

正規化

正規化処理にもいくつか種類があります。

  • 中心化: 平均値が0になるようにする。

  • 標準化: 平均値が0、かつ標準偏差が1になるようにする。

Q 正規化

ホテルの予約レコードの予約人数と合計金額を平均0、標準偏差1になるように正規化するというお題です。 この正規化にはStatsBase.jlzscoreが使えます。

using StatsBase
transform!(reserve_df,:people_num=>zscore, :total_price=>zscore)
first(reserve_df,10) |> println
10×12 DataFrame
 Row │ reserve_id  hotel_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price  total_price_log  people_num_zscore  total_price_zscore
     │ String7     String7   String7      String31             Dates.Date    Dates.Time    Dates.Date     Int64       Int64        Float64          Float64            Float64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ r1          h_75      c_1          2016-03-06 13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200         1.99211            1.30055          -0.0531874
   2 │ r2          h_219     c_1          2016-07-16 23:39:55  2016-07-20    11:30:00      2016-07-21              2        20600         1.33445           -0.483693         -0.74773
   3 │ r3          h_179     c_1          2016-09-24 10:03:17  2016-10-19    09:00:00      2016-10-22              2        33600         1.53908           -0.483693         -0.629857
   4 │ r4          h_214     c_1          2017-03-08 03:20:10  2017-03-29    11:00:00      2017-03-30              4       194400         2.29092            1.30055           0.828138
   5 │ r5          h_16      c_1          2017-09-05 19:50:37  2017-09-22    10:30:00      2017-09-23              3        68100         1.83948            0.408427         -0.317041
   6 │ r6          h_241     c_1          2017-11-27 18:47:05  2017-12-04    12:00:00      2017-12-06              3        36000         1.5682             0.408427         -0.608096
   7 │ r7          h_256     c_1          2017-12-29 10:38:36  2018-01-25    10:30:00      2018-01-28              1       103500         2.01912           -1.37581           0.00393554
   8 │ r8          h_241     c_1          2018-05-26 08:42:51  2018-06-08    10:00:00      2018-06-09              1         6000         0.845098          -1.37581          -0.88011
   9 │ r9          h_217     c_2          2016-03-05 13:31:06  2016-03-25    09:30:00      2016-03-27              3        68400         1.84136            0.408427         -0.314321
  10 │ r10         h_240     c_2          2016-06-25 09:12:22  2016-07-14    11:00:00      2016-07-17              4       320400         2.50705            1.30055           1.9706

なお、zscoreでは標本標準偏差が適用されることに意識しておく必要があります。 また、

😲 Note

全ての列に適用するときはmapcolsあるいはmapcols!が使えます。

mapcols(zscore,df)

また、最小値0、最大値1でスケーリングするにはStatsBase.jlにUnitRangeTransormが用意されています。

standardize(UnitRangeTransform, [0.0 -0.5 0.5; 0.0 1.0 2.0], dims=2)
2×3 Matrix{Float64}:
 0.5  0.0  1.0
 0.0  0.5  1.0

ただ、DataFrameには適用できないようなので、もしやりたいなら次のように適当な関数を作って適用したほうがいいでしょう。

unitrangetransform(v)=(v-minimum(v))/(maximum(v)-minimum(v))
transform(df, :colname=>unitrangetransform)

外れ値の除去

標準化後に絶対値が3を超えないデータだけを採用します。

Q 標準偏差基準の外れ値の除去

zscoreを適用した列に対して、絶対値が3以下のデータをフィルターしています。

reserve_df2 = @chain reserve_df begin
      transform(:total_price=>zscore)
      filter(:total_price_zscore=>(c->abs(c)≤3),_)
end

first(reserve_df2,10) |> println
10×12 DataFrame
 Row │ reserve_id  hotel_id  customer_id  reserve_datetime     checkin_date  checkin_time  checkout_date  people_num  total_price  total_price_log  people_num_zscore  total_price_zscore
     │ String7     String7   String7      String31             Dates.Date    Dates.Time    Dates.Date     Int64       Int64        Float64          Float64            Float64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ r1          h_75      c_1          2016-03-06 13:09:42  2016-03-26    10:00:00      2016-03-29              4        97200         1.99211            1.30055          -0.0531874
   2 │ r2          h_219     c_1          2016-07-16 23:39:55  2016-07-20    11:30:00      2016-07-21              2        20600         1.33445           -0.483693         -0.74773
   3 │ r3          h_179     c_1          2016-09-24 10:03:17  2016-10-19    09:00:00      2016-10-22              2        33600         1.53908           -0.483693         -0.629857
   4 │ r4          h_214     c_1          2017-03-08 03:20:10  2017-03-29    11:00:00      2017-03-30              4       194400         2.29092            1.30055           0.828138
   5 │ r5          h_16      c_1          2017-09-05 19:50:37  2017-09-22    10:30:00      2017-09-23              3        68100         1.83948            0.408427         -0.317041
   6 │ r6          h_241     c_1          2017-11-27 18:47:05  2017-12-04    12:00:00      2017-12-06              3        36000         1.5682             0.408427         -0.608096
   7 │ r7          h_256     c_1          2017-12-29 10:38:36  2018-01-25    10:30:00      2018-01-28              1       103500         2.01912           -1.37581           0.00393554
   8 │ r8          h_241     c_1          2018-05-26 08:42:51  2018-06-08    10:00:00      2018-06-09              1         6000         0.845098          -1.37581          -0.88011
   9 │ r9          h_217     c_2          2016-03-05 13:31:06  2016-03-25    09:30:00      2016-03-27              3        68400         1.84136            0.408427         -0.314321
  10 │ r10         h_240     c_2          2016-06-25 09:12:22  2016-07-14    11:00:00      2016-07-17              4       320400         2.50705            1.30055           1.9706

主成分分析による次元圧縮

主成分分析によって、データの次元を圧縮するお題です。

Q 主成分分析による次元圧縮

PCAはMultivariateStats.jlで実装されています。 注意点は、データ行列を転置して渡す必要があることです。

production_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/production.csv"
production_df = @chain production_url Downloads.download CSV.File DataFrame
import MultivariateStats
X = @chain production_df begin
    select(:length,:thickness)
    Matrix
    transpose
end
M = MultivariateStats.fit(MultivariateStats.PCA, X)
MultivariateStats.predict(M, X)

数値の補完

この節は欠損値の取り扱いに関してまとめています。 欠損にはいくつか種類があるようです。

  • MCAR: 完全にランダムな欠損

  • MAR: ほかの項目に依存した欠損

  • MNAR: 欠損したデータに依存した欠損

一番簡単な対処方法は欠損値を削除してしまうことです。 その他の対処方法としては、定数による補完、集計値(平均値・中央値など)による補完、欠損していないデータに基づく予測値によって補完、時系列の関係から補完などがあるようです。

Q 欠損レコードの削除

対象レコードはthicknessに欠損が存在する製造レコードです。 まずデータを読み込む際に、欠損値を表す文字列をしているようにキーワード引数を指定します。 欠損値の除去はdropmissing関数で簡単に行えます。

production_missing_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/production_missing_num.csv"
production_missing_df = @chain production_missing_url begin
    Downloads.download 
    CSV.File(;missingstring="None") 
    DataFrame
end

@chain dropmissing(production_missing_df)  first(10) println
10×4 DataFrame
 Row │ type     length    thickness  fault_flg
     │ String1  Float64   Float64    Bool
─────┼─────────────────────────────────────────
   1 │ E        274.027    40.2411       false
   2 │ D         86.3193   16.9067       false
   3 │ E        123.94      1.01846      false
   4 │ B        175.555    16.4149       false
   5 │ B        244.935    29.0611       false
   6 │ B        226.427    39.7638       false
   7 │ C        331.638    16.8356       false
   8 │ A        200.865    12.1843       false
   9 │ C        276.387    29.8996       false
  10 │ E        168.441     1.26592      false

Q 定数補完

欠損値を1に置き換えます。 missingを置き換えるには、SQLの例と同じくcoalesceが利用可能です。

@chain production_missing_df begin
    transform(:thickness=>ByRow(c->coalesce(c,1))=>:thickness)
    first(30) 
    println
end
30×4 DataFrame
 Row │ type     length    thickness  fault_flg
     │ String1  Float64   Real       Bool
─────┼─────────────────────────────────────────
   1 │ E        274.027   40.2411        false
   2 │ D         86.3193  16.9067        false
   3 │ E        123.94     1.01846       false
   4 │ B        175.555   16.4149        false
   5 │ B        244.935   29.0611        false
   6 │ B        226.427   39.7638        false
   7 │ C        331.638   16.8356        false
   8 │ A        200.865   12.1843        false
   9 │ C        276.387   29.8996        false
  10 │ E        168.441    1.26592       false
  11 │ D        218.139   33.1354        false
  12 │ E        215.179   41.7599        false
  13 │ D        218.038   11.8363        false
  14 │ E        218.31    39.5786        false
  15 │ D        207.823   38.7026        false
  16 │ E        286.537    5.3755        false
  17 │ C        239.284   18.9418        false
  18 │ C        373.655   11.2548        false
  19 │ E        200.112   17.9743        false
  20 │ C        326.175   44.3079        false
  21 │ B        157.573    0.409495      false
  22 │ B        150.217    8.68243       false
  23 │ C        358.605   25.7004        false
  24 │ C        269.006   16.9575        false
  25 │ B        202.549    1             false
  26 │ D        223.806   19.6508        false
  27 │ B        263.844   34.6643        false
  28 │ B        169.691    1             false
  29 │ A        131.849    4.7216         true
  30 │ E        128.99    16.5232        false

Q 平均値補完

欠損値に平均値を代入するお題です。 平均をmean関数で計算するにはビルトインモジュールのStatistics.jlをインポートする必要があります。 しかし、欠損値を含んだままで平均を計算すると、計算結果もmissingになってしまいます。 各種統計量をまとめて計算可能なdescribe関数を利用すれば、欠損値があっても平均値を計算可能なので、これを利用することにします。

平均値を計算した後は、定数補完と同様の処理が可能です。 今回はChain.jl@asideマクロを使って、平均値の計算をパイプライン処理の中に入れてました。

@chain production_missing_df begin
    @aside M = describe(_, :mean,cols=:thickness) # 平均値を計算
    @aside thickness_mean = M[!,:mean][1] # データフレームから平均値を取り出す
    transform(:thickness=>ByRow(c->coalesce(c,thickness_mean))=>:thickness) # 欠損値を平均値で置き換え
    first(30) 
    println
end

30×4 DataFrame
 Row │ type     length    thickness  fault_flg
     │ String1  Float64   Float64    Bool
─────┼─────────────────────────────────────────
   1 │ E        274.027   40.2411        false
   2 │ D         86.3193  16.9067        false
   3 │ E        123.94     1.01846       false
   4 │ B        175.555   16.4149        false
   5 │ B        244.935   29.0611        false
   6 │ B        226.427   39.7638        false
   7 │ C        331.638   16.8356        false
   8 │ A        200.865   12.1843        false
   9 │ C        276.387   29.8996        false
  10 │ E        168.441    1.26592       false
  11 │ D        218.139   33.1354        false
  12 │ E        215.179   41.7599        false
  13 │ D        218.038   11.8363        false
  14 │ E        218.31    39.5786        false
  15 │ D        207.823   38.7026        false
  16 │ E        286.537    5.3755        false
  17 │ C        239.284   18.9418        false
  18 │ C        373.655   11.2548        false
  19 │ E        200.112   17.9743        false
  20 │ C        326.175   44.3079        false
  21 │ B        157.573    0.409495      false
  22 │ B        150.217    8.68243       false
  23 │ C        358.605   25.7004        false
  24 │ C        269.006   16.9575        false
  25 │ B        202.549   19.4704        false
  26 │ D        223.806   19.6508        false
  27 │ B        263.844   34.6643        false
  28 │ B        169.691   19.4704        false
  29 │ A        131.849    4.7216         true
  30 │ E        128.99    16.5232        false
19.4704というのが平均値です。

公式パッケージには次の多重代入法に関連するImpute.jlというパッケージがあって、こちらを使うともう少しエレガントに処理が可能です。

using Impute
@chain production_missing_df begin
    Impute.substitute(;statistic=Impute.mean)
end

Q PMMによる多重代入

PMM(Predictive Mean Matching)は、残念ながらImpute.jlでは実装されていないようです。[1] (逆に実装するチャンスともいえます。) おとなしく、RCallやPyCallで処理を呼び出すか、Impute.jlの別の補完法を使うのが良さそうです。

[1] 他のパッケージもあるにはあるのですが、RCallでwrapしたもののようです。

つづく