JuliaでLightGBMしようとしたら意味が分からなかったのでランダムフォレストで我慢した

# はじめに
Juliaを2つ目の言語として勉強を始め、意味が分からず挫折しそうになりました。
前回の記事で手始めにタイタニックのデータを使ってLightGBMモデルを作成しようとしたら全く意味が分からず、気付けばコントローラーを握った自分がいました。前回記事↓

https://qiita.com/inoshun/items/6c49347c857931703f2c

さすがに、勉強し始めて1日でリタイアするのはプライドが許さなかったので、とりあえず別のモデルでも試すか！ということでランダムフォレストでモデルを作成したのでまとめようと思います。

また、今回はJuliaの書き方についても丁寧にまとめていきたいと思います。
なぜなら、記事が圧倒的に少なくて、目的の情報が**大きなスーパーの中から塩昆布を探す**くらい見当たりません。
まじでどこにあるんや...って探し当てたとしても英語の記事ばかり。~~くそくらえ~~
ということで、今後Juliaの人気がさらに出てきた際に困る人が1人でも少なくなってくれればということを願い記事にしていきたいと思います。

# Juliaの書き方
ランダムフォレストのモデルを作成する前のデータ前処理の段階からコードを説明していこうと思います。
また、前回はjlファイルを作成して、REPL（インタラクティブモード）で出力させていましたが、今回はJupyter Notebookでも実行できるみたいなので、そっちで動かしていきます。
それではいきましょう！

### 必要なパッケージをインポート
JuliaはPythonライクな書き方ができ、かつC言語のように高速であることが特徴です。
Pythonと同じように、まずは必要なパッケージをインポートします（Juliaはパッケージにおんぶにだっこです）。

```julia
using Pkg
Pkg.add("パッケージ名")
using パッケージ名
```

パッケージをインポートするためのパッケージである``Pkg``をまずは``using Pkg``でインポートします。
その後、``Pkg.add("パッケージ名")``で必要なライブラリをインストールします。
今回は、以下のパッケージをインストールします。

- **DataFrames**：R言語（data.frame）やPython（pandas DataFrame）のデータフレームに似たデータ構造を提供。列志向のデータ構造を通じて、様々な操作を行うことが可能。
- **CSV**：csvファイルを読み書きする。
- **Statistics**：標準ライブラリの一部であり、基本的な統計関数（平均、中央値、標準偏差など）を提供。
- **StatsBase**：基本的な統計ツールとデータ型を提供。平均、分散、標準偏差などの計算に加えて、より高度な統計手法も含まれる。
- **CategoricalArrays**：カテゴリーデータを効率的に扱うことができる。限定された数のカテゴリーを持つデータ（性別や国籍など）のための特殊な配列タイプを提供。
- **MLJ**：Machine Learning in Juliaの略で、機械学習フレームワーク。機械学習アルゴリズムへのアクセス、データの前処理、モデルの訓練、評価およびハイパーパラメータの調整機能を提供。
- **MLJDecisionTreeInterface**：MLJフレームワーク内で使用するための決定木モデルへのインターフェースを提供。

これらをパッケージ名にぶち込んでインポートしてください。

---
### データを読み込む
それでは、データを読み込んでいきましょう。

```julia
df = CSV.read("your_file_path", DataFrame)
first(df, 5)
```

わたしはPythonしか知りませんが、かなりPythonに近い書き方だなと感じました。
CSVパッケージを用いて、ファイルを指定し、データフレームとして読み込んでるんだなって感覚でわかるかと思います。
また、``first(df,n)``で最初のn行を表示できます。
Pythonの``df.head()``と同じ感じです。
ちなみに、最後の行を表示したいなら``last(df,n)``、特定の範囲であれば``df[6:10,:]``のようにして6行目から10行目を表示、ランダムな行を表示するには``sample(df,n)``です。sapmle関数を用いる際は`Random`パッケージをインストール、インポートしてから使用しましょう。
さらに、ちなみにJuliaはインデックスが1から始まります。Pythonを使ってる方はインデックスが0からなので少し違和感があるかもしれませんが、**0から数えるより1から数えた方が圧倒的に楽**です。

表示されたデータフレームはこんな感じです。

![deta.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/ef8bad67-0e40-ebec-150b-e55ae4270cde.png)

普通に感動したんですけど、カラム名の下に型が書いてあるんですよね。
型調べなくてもいいの！？ってテンションあがりました。
まあ、型変更したりした場合は確認したりした方がいいですけどね。
ちなみに型の確認は``typeof()``です。
``typeof(df.Name)``または``typeof(df[!,:Name])``でNameカラムの型を調べることができます。
データフレームの各カラムへのアクセスは``df[!,:カラム名]``の方が都合がいいのでこちらを覚えましょう。
``!``はカラムに対してアクセスし操作をするぞという合図で、操作したいカラムを指定する場合は``:カラム名``で指定します。

---

### データの前処理

まずはいらないカラムを削除します。
```julia
dropcols = [:PassengerId, :Name, :Ticket, :Cabin]
df = select(df, Not(dropcols))
```
削除したいカラムのリストを指定してあげて、select関数でデータフレームの削除したいカラム以外を選択してる感じですね。これも感覚的に何をしているのかわかるんじゃないでしょうか？

次に、欠損値の確認をしていきます。
```julia
for col_name in names(df)
    missing_count = sum(ismissing, df[!,col_name])
    if missing_count > 0
        println("$col_name: $missing_count missing values")
    end
end
```
Pythonのfor文、if文に似てますね。
``names(df)``でデータフレームの全カラム名をリストで取得できます。
``sum(ismissing, df[!,col_name])``で各カラムの欠損値が``true``である合計値を算出します。
その後、
```julia
if missing_count > 0
    println("$col_name: $missing_count missing values")
```
で欠損値の合計が1以上であるカラム名の欠損値の数を出力させています。
また、for文とif文を書いた後は``end``で終了させておきましょう。

では、欠損値の補完をしていきましょう。今回は、Ageカラムは中央値、Embarkedカラムは最頻値で補完させます。
```julia
median_age = median(skipmissing(df[!,:Age]))
df[!,:Age] = coalesce.(df[!,:Age], median_age)

mode_Embarked = mode(skipmissing(df[!,:Embarked]))
df[!,:Embarked] = coalesce.(df[!,:Embarked], mode_Embarked)
```
ちょっと難しいですが、median関数とmode関数を用いて、中央値と最頻値を算出し、それをcoalesce関数で各カラムの欠損値に対して置換させます。

最後の処理として、SexカラムとEmbarkedカラムのデータが文字列型なので、文字列→カテゴリー→整数値に変換していきます。
要するにラベルエンコーディングさせてます。

```julia
df[!,:Sex] = categorical(df[!,:Sex])
df[!,:Embarked] = categorical(df[!,:Embarked])

df[!,:Sex] = CategoricalArrays.levelcode.(df[!,:Sex])
df[!,:Embarked] = CategoricalArrays.levelcode.(df[!,:Embarked])
```
なんかよくわかりませんがこれでいけます（暴論）。
この辺りはもう少し勉強します！

ここで、謎コードを書いていきます。
なぜ書くのかは後程モデルを学習させたときに説明します。
```julia
y = map(val -> Int(val=="group1"), y)
y = identity(y)
y = categorical(y)
```
**マジでわからん**
後程詳しく書きますが、カテゴリー型に再度更新させてます。
もう一回言わせてもらう。**マジでわからん**

---
### ランダムフォレストモデルを作成
まずは特徴量とターゲットを指定します。
```julia
X = select(df, Not(:Survived))
y = df[!,:Survived]
```
データフレームからSurvivedカラム以外を特徴量、Survivedカラムをターゲットに指定しました。

次にデータの分割を行います。
```julia
train, test = partition(eachindex(y), 0.7, shuffle=true)
```
partition関数を用いて、データを分割します。
引数に、`y`のインデックスを70％：30%で分割し、分割はランダムで行うよって感じですね。
なぜ`y`を指定してるのかわかりませんが、`y`意外だとエラーでます。
ここでも言わせてくれ。**マジでわからん**

ではモデルをロードして起動させましょう。
```julia
model = @load DecisionTreeClassifier pkg=DecisionTree
model = model()
```
MLJDecisionTreeClassifierパッケージから`DecisionTree`モデルをロードしてるんだと思います。
そして、それを起動？
まあ、よく見かけますが決まり文句みたいなもんです。

その後、モデルを学習させます。
```julia
tree = machine(model, X, y)
fit!(tree, rows=train, force=true)
```
引数の`force=true`は学習を上書きするかどうかですね。
学習させたモデルを更新させたいときは`true`を指定しておきましょう。

**また、この`fit関数`、ここで先ほど言っていた、なぜカテゴリー型に更新したかの問題が出てきます。**

https://github.com/alan-turing-institute/MLJ.jl/issues/423

上記ページで議論されてますが、原因がわかってないそうです。
カテゴリー型に更新せずに実行してしまうと
```
DomainError with 0:
Can only convert categorical elements to integers. 
```
というエラーが発生し、カテゴリー型のデータを整数値に変換しようとしているらしいが、カテゴリー型のまま渡しても、整数値に変換して渡しても同じエラーが発生するだけです。
しかし、再度カテゴリー型に更新することでエラーは解消されます。
~~**は？**~~
また、開発者らしき方が、`y=categorical(y)`の前に`y=identity(y)`をすることで`eltype`を強化できると発言してるので、一応私も真似てみました(*ﾉωﾉ)
ちなみに、`eltype`とは一言で説明するのであれば、「要素型」を返す関数です。
この辺りは追々理解します。


次に学習させたモデルを用いて予測値を算出します。
```julia
yhat = MLJ.predict(tree, rows=test)
```
ここまじで解決するのに時間がかかったんですけど、
公式のMLJ.jlのドキュメントには`predict(tree, X[test,:])`と書いていましたが、一生`predict`が定義されてないってエラーが出てました。以下、ドキュメントとかです。

https://juliapackages.com/p/mlj

https://docs.juliahub.com/MLJ/rAU56/0.15.2/getting_started/

そしていろいろ調べていると、

https://qiita.com/mametank/items/29f89cbaa819a8e29577

どうも変数スコープというのがあるみたいで、例えば
```julia
s = 0
for i in 1:10
    s = s + 1
end
```
というコードを実行するとエラーが生じます。`s`なんか定義してねえよ。と。
いや`s=0`でしてんじゃんと思いましたが、どうもfor文の中で`s`を変更したかったら、以下のようにグローバル変数としなけらばならないみたいです。

```julia
s = 0
for i in 1:10
  grobal  s = s + 1
end
``` 
ここからインスピレーションを受けて、「MLJ.jlの中に関数としてあるけど、スコープの関係からMLJの外にあるんじゃね？」と考え、`MLJ.predict`と変更したところうまくいきました。
エラーが解消したときは、思わずガッツポーズしちゃいましたね。


最後にモデルの評価を行います。
今回はAccuracy（精度）を評価します。
```julia
accuracy(yhat, y[test])
```
これで算出されたAcuuracyはなんと

![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/c6dc1ebd-d040-8057-038f-456d5f3d2261.png)

(´・ω・)ん?
(*っдс) ｺﾞｼｺﾞｼ

![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/8398ab4b-c642-be94-b3d4-05851cdbec83.png)

### まとめ
皆さんお疲れさまでした。
いかがだったでしょうか。
Juliaで機械学習モデルを作成しましたが、書き終えるまでにめちゃくちゃ調べましたが、いざ書き終えると大したこと書いてないなって感じでした。
なんといっても参考記事の少ないこと！
まじで英語の記事しかないし、それすらも少ないのでドキュメント漁るしか方法はありません。
と言いたいところですが、さすがChatGPTですね。
今回、GPTsを用いて、Juliaコードを書くことに特化させたGPTを作成しました。
まあ、情報の少なさから精度はあてになりませんが…
けど、今回の記事のコードはほとんどこいつがやってくれました。
URLを貼っておくので、Juliaやってみたいよ！って方は是非使ってみてください。
それでは！！！

https://chat.openai.com/g/g-jbwzUYl7m-julia-pro-coder

最後にコード全体を貼っておきます。
```julia
using Pkg
Pkg.add("DataFrames")
Pkg.add("CSV")
Pkg.add("MLJ")
Pkg.add("CategoricalArrays")
Pkg.add("Statistics")
Pkg.add("StatsBase")
Pkg.add("MLJDecisionTreeInterface")
using DataFrames, CSV, MLJ, CategoricalArrays, Statistics, StatsBase, MLJDecisionTreeInterface

df = CSV.read("./Unity_Share_Hub/data/titanic_data.csv", DataFrame)
first(df,5)

for col_name in names(df)
  missing_count = sum(ismissing, df[!,col_name])
  if missing_count > 0
    println("$col_name: $missing_count missing values")
  end
end

median_age = median(skipmissing(df[!, :Age]))
df[!, :Age] = coalesce.(df[!, :Age], median_age)

mode_embarked = mode(skipmissing(df[!, :Embarked]))
df[!, :Embarked] = coalesce.(df[!, :Embarked], mode_embarked)

df[!, :Sex] = categorical(df[!, :Sex])
df[!, :Embarked] = categorical(df[!, :Embarked])

df[!, :Sex] = CategoricalArrays.levelcode.(df[!, :Sex])
df[!, :Embarked] = CategoricalArrays.levelcode.(df[!, :Embarked])

dropcols = [:PassengerId, :Name, :Cabin, :Ticket]
df = select(df, Not(dropcols))

y = df[!, :Survived]
X = select(df, Not(:Survived))

y = map(val -> Int(val=="group1"), y)
y = identity.(y)
y = categorical(y)
train, test = partition(eachindex(y), 0.7, shuffle=true)

model = @load DecisionTreeClassifier pkg=DecisionTree
model = model()

tree = machine(model, X, y)
fit!(tree, rows=train, force=true)

yhat = MLJ.predict(tree, rows=test)
accuracy(yhat, y[test])
```