juliaで前処理大全 9.カテゴリ型

juliaで前処理大全その8です。今回はカテゴリ型を取り扱います。

カテゴリ型への変換

Q カテゴリ型の変換

CategoricalArrays.jlを使うとカテゴリ型に変換できます。

using DataFrames,CSV,Chain,Downloads
using CategoricalArrays
customer_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/customer.csv"

customer_df = @chain customer_url begin 
                    Downloads.download 
                    CSV.File 
                    DataFrame
                    DataFrames.transform(:sex=>categorical=>:sex_c)
                    end
first(customer_df,10) |> println
10×6 DataFrame
 Row │ customer_id  age    sex      home_latitude  home_longitude  sex_c
     │ String7      Int64  String7  Float64        Float64         Cat…
─────┼───────────────────────────────────────────────────────────────────
   1 │ c_1             41  man            35.0922         136.512  man
   2 │ c_2             38  man            35.3251         139.411  man
   3 │ c_3             49  woman          35.1205         136.511  woman
   4 │ c_4             43  man            43.0349         141.24   man
   5 │ c_5             31  man            35.1027         136.524  man
   6 │ c_6             52  man            34.4408         135.39   man
   7 │ c_7             50  man            43.0158         141.231  man
   8 │ c_8             65  woman          38.2013         140.466  woman
   9 │ c_9             36  woman          33.3228         130.331  woman
  10 │ c_10            34  woman          34.2904         132.303  woman

カテゴリ型のマスターデータには、Rの場合と同じく、levels関数でアクセスできます。

@show levels(customer_df.sex_c)
levels(customer_df.sex_c) = InlineStrings.String7["man", "woman"]

カテゴリ型のインデックスを取得するにはrefsというフィールドを取得すれば、閲覧可能です。 ただし、UInt型なので、Int型に変換して見やすくしています。

@show customer_df.sex_c.refs .|> Int
customer_df.sex_c.refs .|> Int = [1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1, 1, 2, 1, 1, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 2, 1, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 2, 2, 1, 1, 1, 2, 1, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 2, 2, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 1, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 2, 2, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 2, 2, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 1, 2, 2, 1, 1, 2, 1, 2, 2, 1, 2, 2, 1, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 1, 1, 1, 2, 1, 2, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 1, 1, 2, 1, 2, 1, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 1, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 1, 2, 1, 2, 1, 1, 2, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1]

インデックスの順番を自分好みに変えることもできます。

levels(customer_df.sex_c, ["woman","man"])

ダミー変数化

Q ダミー変数化

文字のカテゴリをダミー変数化します。いわゆるOne hot encodingという処理です。 juliaではいくつかこの機能を備えたパッケージはありますが、 大して難しいコードでもないので、パイプライン処理の中に自分で書いてしまっても良さそうです。 こちらのdiscourseの議論を参考にしてみました。ski先生流の書き方ならこのようになります。

@chain customer_df begin
    select(:sex, [:sex=>ByRow(isequal(v))=>Symbol("sex_is_"*v) for v in unique(customer_df.sex)])
    first(10)
    println
end

10×3 DataFrame
 Row │ sex      sex_is_man  sex_is_woman
     │ String7  Bool        Bool
─────┼───────────────────────────────────
   1 │ man            true         false
   2 │ man            true         false
   3 │ woman         false          true
   4 │ man            true         false
   5 │ man            true         false
   6 │ man            true         false
   7 │ man            true         false
   8 │ woman         false          true
   9 │ woman         false          true
  10 │ woman         false          true
パイプライン処理の中で行えるので、Awesomeだと思います。

行列とブロードキャストを利用したエレガントな別解もあります。

customer_df.sex .== permutedims(unique(customer_df.sex))
😲 Note
permutedimsはベクトルを1行のマトリックスに変換してくれるbuiltin関数です。

カテゴリ値の集約

Q カテゴリ値の集約

60歳以上のカテゴリ値を1つに集約するお題です。 まず、数値型の例と同様にCategoricalArrays.jlを使ってカテゴリ型に変更します。 CategoricalArrayがAny型をサポートしていないため、replace!関数で60歳以上のカテゴリを60に入れてしまうことにしました。

transform!(customer_df, :age=>ByRow(c->10floor(Int,c/10))=>:age_rank)
transform!(customer_df, :age_rank=>categorical=>:age_rank)
replace!(customer_df.age_rank,[70,80]=>60)
first(customer_df,20) |> println

20×7 DataFrame
 Row │ customer_id  age    sex      home_latitude  home_longitude  sex_c  age_rank
     │ String7      Int64  String7  Float64        Float64         Cat…   Cat…
─────┼─────────────────────────────────────────────────────────────────────────────
   1 │ c_1             41  man            35.0922         136.512  man    40
   2 │ c_2             38  man            35.3251         139.411  man    30
   3 │ c_3             49  woman          35.1205         136.511  woman  40
   4 │ c_4             43  man            43.0349         141.24   man    40
   5 │ c_5             31  man            35.1027         136.524  man    30
   6 │ c_6             52  man            34.4408         135.39   man    50
   7 │ c_7             50  man            43.0158         141.231  man    50
   8 │ c_8             65  woman          38.2013         140.466  woman  60
   9 │ c_9             36  woman          33.3228         130.331  woman  30
  10 │ c_10            34  woman          34.2904         132.303  woman  30
  11 │ c_11            73  man            33.3056         130.301  man    60
  12 │ c_12            46  man            34.3033         132.304  man    40
  13 │ c_13            47  woman          43.0126         141.242  woman  40
  14 │ c_14            79  woman          43.0146         141.225  woman  60
  15 │ c_15            25  woman          43.0219         141.242  woman  20
  16 │ c_16            63  woman          34.3132         132.281  woman  60
  17 │ c_17            48  woman          35.0855         136.555  woman  40
  18 │ c_18            72  man            43.0131         141.27   man    60
  19 │ c_19            38  man            34.4756         135.364  man    30
  20 │ c_20            43  woman          34.4411         135.393  woman  40
😕 Warning!

StringとNumberが混在するAny型の配列をCategoricalArrayに変換しようとするとエラーがでます。

categorical([1,2,"3"])
ArgumentError: CategoricalArray only supports AbstractString, AbstractChar and Number element types (got element type Any)

カテゴリ値の組み合わせ

Q カテゴリ値の組み合わせ

年代と性別の組み合わせを使ってカテゴリ値を作るお題です。 string関数を使って簡単に実装できます。

@chain customer_df begin
    select(:age,:sex, [:age,:sex]=>ByRow((x,y)->string(floor(Int,x/10)*10, "_", y))=>:sex_and_age)
    DataFrames.transform(:sex_and_age=>categorical=>:sex_and_age)
    first(10)
    println
end
10×3 DataFrame
 Row │ age    sex      sex_and_age
     │ Int64  String7  Cat…
─────┼─────────────────────────────
   1 │    41  man      40_man
   2 │    38  man      30_man
   3 │    49  woman    40_woman
   4 │    43  man      40_man
   5 │    31  man      30_man
   6 │    52  man      50_man
   7 │    50  man      50_man
   8 │    65  woman    60_woman
   9 │    36  woman    30_woman
  10 │    34  woman    30_woman

カテゴリ型の数値化

Q カテゴリ型の数値化

製造レコードから種別ごとに、平均障害率を計算するというお題です。 種別ごとに計算するのはgroupbyを使って実装できます。 自分自身のレコードを除かなければならないというのが少々曲者です。 今回は、ブロードキャスト演算子を使った無名関数で実装してみました。

production_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/production.csv"

production_df = 
    @chain production_url begin 
        Downloads.download 
        CSV.File 
        DataFrame
        groupby(:type)
        transform(:fault_flg=>(c->(sum(c) .- c)/(length(c)-1))=>:fault_flg_per_type)
    end

first(production_df,10) |> println
10×5 DataFrame
 Row │ type     length    thickness  fault_flg  fault_flg_per_type
     │ String1  Float64   Float64    Bool       Float64
─────┼─────────────────────────────────────────────────────────────
   1 │ E        274.027    40.2411       false           0.0612245
   2 │ D         86.3193   16.9067       false           0.0327103
   3 │ E        123.94      1.01846      false           0.0612245
   4 │ B        175.555    16.4149       false           0.0344828
   5 │ B        244.935    29.0611       false           0.0344828
   6 │ B        226.427    39.7638       false           0.0344828
   7 │ C        331.638    16.8356       false           0.0761905
   8 │ A        200.865    12.1843       false           0.0547264
   9 │ C        276.387    29.8996       false           0.0761905
  10 │ E        168.441     1.26592      false           0.0612245

カテゴリ型の補完

Q KNNによる補完

まずデータを準備します。typeの列で欠損が発生しています。 欠損のないデータについてまずKNNで学習し、欠損のあるデータに適用するという流れです。 今回はjuliaにおけるScikitLearnのような存在であるMLJを利用してみたいと思います。[1]

いつも通り、データの読み込みを行いますが、KNNを適用する準備として、:typeの列はcategorical型に変換します。 また、学習に利用するデータとして:length:thicknessを選んでおきます。

production_missc_url = "https://raw.githubusercontent.com/hanafsky/awesomebook/master/data/production_missing_category.csv"

production_missc_df = 
    @chain production_missc_url begin 
        Downloads.download 
        CSV.File 
        DataFrame
        select(:type=>categorical=>:type,
              :length,:thickness)
    end

first(production_missc_df,10) |> println
10×3 DataFrame
 Row │ type     length    thickness
     │ Cat…?    Float64   Float64
─────┼──────────────────────────────
   1 │ E        274.027    40.2411
   2 │ D         86.3193   16.9067
   3 │ E        123.94      1.01846
   4 │ B        175.555    16.4149
   5 │ B        244.935    29.0611
   6 │ B        226.427    39.7638
   7 │ C        331.638    16.8356
   8 │ A        200.865    12.1843
   9 │ missing  276.387    29.8996
  10 │ E        168.441     1.26592

さて、データを訓練データとテストデータに分け、KNNで学習するようにしてみます。 出力を補完値にするために``predict``関数ではなく、``predict_mode``関数を使いました。 それなりにAwesomeにかけたのではないでしょうか。

using MLJ
KNNClassifier = @load KNNClassifier pkg=NearestNeighborModels verbosity=0

train = dropmissing(production_missc_df)
test = filter(:type=>ismissing, production_missc_df)
y, X = unpack(train, ==(:type))
test[!,:type] = @chain machine(KNNClassifier(K=3), X, y) begin 
                    fit!
                    predict_mode(test[!, Not(:type)])
                end
first(test,10) |> println
10×3 DataFrame
 Row │ type  length    thickness
     │ Cat…  Float64   Float64
─────┼───────────────────────────
   1 │ E     276.387    29.8996
   2 │ E     263.844    34.6643
   3 │ E     129.365    21.3468
   4 │ A     203.379    30.2865
   5 │ E     157.463    11.1662
   6 │ A     122.947     5.69413
   7 │ D     123.976    11.1189
   8 │ A     107.941    14.7408
   9 │ A     189.611     1.41968
  10 │ D      85.5865   16.9135

[1] MLJについては、有名なパッケージではありますが、日本語での使い方をまとめた記事はあまりないので、そのうちにMLJのみを単独で取り上げてみたいと思います。

つづく