ランダムフォレスト（回帰と分類）を実装してみた（決定木はscikit-learnを使う）．

# 背景・目的
今回は，ランダムフォレストの技術的な理解のためにブートストラップサンプリングと結果出力（多数決と平均値）の部分をを自身で実装していきます．scikit-learnを用いれば一発で終わりますが．．．まぁ，技術的な理解も必要ということで．ただし，決定木の部分はscikit-learnを用います．
先行事例では，ランダムフォレストの分類が実装されています（じょんすみす, 2017, ひろきし, 2020, deaikei, 2016）．本記事では，先行事例を参考に分類と回帰の実装を行いました．

# ランダムフォレストとは？（超簡単な説明）
ランダムフォレストは，下記のようなアルゴリズムです．
『ランダムフォレストは，複数の分類木や回帰木の結果をあわせて分類や回帰を行うアンサンブル学習アルゴリズムである．このアルゴリズムは，まず多数のブートストラップサンプルを抽出する．次に，各ブートストラップサンプルに対する決定木モデルを生成する．各決定木で使用する特徴量はランダムに選択した少数の変数のみである．最終的に，複数の決定木による結果が得られるため，回帰の時には平均値，分類の時には多数決を行うことにより出力結果を得る．決定木は，木が深くなると構造が複雑になり，過学習が起きやすいという問題がある．しかし，ランダムフォレストは，バギングにより決定木よりも汎化性能に優れ，過学習の問題を緩和する．また，ランダムフォレストの重要な特徴の一つは，各入力変数の重要度を評価する機能である．』（上脇ら，2023）
特に，ランダムフォレストやXGBoost，LightGBMといった手法は，各入力変数の重要度が評価できる点が大きなメリットです．この機能は筆者が専門とする農業生産だけでなく，マーケティングなどにおいても力を発揮します．

# ランダムフォレストの実装（分類）
データは，Iris Datasetを用いました．
```python
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from numpy.random import randint
from sklearn import tree
from collections import Counter

from sklearn.datasets import load_iris

# 決定木を作る本数
n_estimators = 100
# 決定木のパラメータ
params = {'criterion':'gini',
          'splitter':'best',
          'max_depth':None,
          'min_samples_split':2,
          'min_samples_leaf':1,
          'min_weight_fraction_leaf':0.0,
          'max_features':None,
          'random_state':314,
          'max_leaf_nodes':None,
          'min_impurity_decrease':0.0,
          'class_weight':None,
          'ccp_alpha':0.0}

def Data_sampling(X_train, n_estimators):
    # 利用するデータをランダムに選択
    ind = [randint(len(X_train), size=len(X_train)) for _ in range(n_estimators)]
    
    # 利用する特徴量をランダムに選択
    col = [randint(len(X_train[0]), size=len(X_train[0])) for _ in range(n_estimators)]

    return ind, col

def Training(X, y, indexes, columns):
    data = X[list(indexes)][:, list(columns)]
    target = y[list(indexes)]
    clf = tree.DecisionTreeClassifier(criterion=params['criterion'],
                                    splitter=params['splitter'],
                                    max_depth=params['max_depth'],
                                    min_samples_split=params['min_samples_split'],
                                    min_samples_leaf=params['min_samples_leaf'],
                                    min_weight_fraction_leaf=params['min_weight_fraction_leaf'],
                                    max_features=params['max_features'],
                                    random_state=params['random_state'],
                                    max_leaf_nodes=params['max_leaf_nodes'],
                                    min_impurity_decrease=params['min_impurity_decrease'],
                                    class_weight=params['class_weight'],
                                    ccp_alpha=params['ccp_alpha'])
    return clf.fit(data, target)

iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, stratify=iris.target,random_state=314)

# データのサンプリング
indexes, columns = Data_sampling(X_train, n_estimators)
# 学習
models = [Training(X_train, y_train, x, y) for x, y in zip(indexes, columns)]
# 予測
predicts = [model.predict(X_test[:, list(y)]) for model, y in zip(models, columns)]

# 最終的な出力
predict = pd.DataFrame(
    {'predict' : [max(Counter(result).items(), key=(lambda x: x[1]))[0] for result in np.array(predicts).T],
    'label' : y_test}
)

predict.groupby(['predict', 'label']).size()
```
```text
predict  label
0        0        13
1        1        10
         2         1
2        1         3
         2        11
```

# ランダムフォレストの実装（回帰）
データは，Diabetes Datasetを用いました．
```python
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from sklearn import tree

from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error

from sklearn.datasets import load_diabetes

np.random.seed(seed=314)

# 決定木を作る本数
n_estimators = 100
# 決定木のパラメータ
params = {'criterion':'squared_error',
          'splitter':'best',
          'max_depth':None,
          'min_samples_split':2,
          'min_samples_leaf':1,
          'min_weight_fraction_leaf':0.0,
          'max_features':None,
          'random_state':314,
          'max_leaf_nodes':None,
          'min_impurity_decrease':0.0,
          'ccp_alpha':0.0}

def Data_sampling(X_train, n_estimators):
    # 利用するデータをランダムに選択
    ind = [np.random.randint(len(X_train), size=X_train.shape[0]) for _ in range(n_estimators)]
    
    # 利用する特徴量をランダムに選択
    col = [np.random.randint(len(X_train[0]), size=X_train.shape[1]) for _ in range(n_estimators)]

    return ind, col

def Training(X, y, indexes, columns):
    data = X[list(indexes)][:, list(columns)]
    target = y[list(indexes)]
    reg = tree.DecisionTreeRegressor(criterion=params['criterion'],
                                    splitter=params['splitter'],
                                    max_depth=params['max_depth'],
                                    min_samples_split=params['min_samples_split'],
                                    min_samples_leaf=params['min_samples_leaf'],
                                    min_weight_fraction_leaf=params['min_weight_fraction_leaf'],
                                    max_features=params['max_features'],
                                    random_state=params['random_state'],
                                    max_leaf_nodes=params['max_leaf_nodes'],
                                    min_impurity_decrease=params['min_impurity_decrease'],
                                    ccp_alpha=params['ccp_alpha'])
    return reg.fit(data, target)

diabetes = load_diabetes()
X_train, X_test, y_train, y_test = train_test_split(diabetes.data, diabetes.target, random_state=314)

# データのサンプリング
indexes, columns = Data_sampling(X_train, n_estimators)

# 学習
models = [Training(X_train, y_train, x, y) for x, y in zip(indexes, columns)]

# 予測
predicts = [model.predict(X_test[:, list(y)]) for model, y in zip(models, columns)]
pd.DataFrame(np.array(predicts).T, columns=['predict_{}'.format(i) for i, predict in enumerate(predicts)])

# 最終的な出力
results = pd.DataFrame(
    {'predicted' : [np.mean(result) for result in np.array(predicts).T],
    'test' : y_test}
)
```
```text
	predicted	test
0	125.48	83.0
1	92.55	80.0
2	225.11	281.0
3	203.11	191.0
4	92.62	53.0
...	...	...
106	203.21	277.0
107	100.93	65.0
108	221.56	150.0
109	208.11	163.0
110	200.99	233.0
```

# まとめ
今回は，ランダムフォレストの技術的な理解のためにブートストラップサンプリングと結果出力（多数決と平均値）の部分をを自身で実装してみました．車輪の再発明ですが，回帰の実装をやった例は少なかったので多少なりともなにかに貢献できる部分はあるのかなと思っています．皆様の参考になれば幸いです．

# 参考文献やサイトなど
* じょんすみす. 2017. ランダムフォレストを実装してみる. https://dev.classmethod.jp/articles/random-forest-impl/, accessed on 22 May 2024.
* ひろきし. 2020. ランダムフォレストをスクラッチで実装したい. https://qiita.com/roki18d/items/1c5633beb80a5c2d58e7, accessed on 22 May 2024.
* deaikei. 2016. 🌲🌲 Python でランダムフォレストを実装する男 🌲🌲. https://qiita.com/deaikei/items/52d84ccfedbfc3b222cb, accessed on 22 May 2024.
* 上脇優人, 福田信二. 2024. ランダムフォレストを用いたハツカダイコンの根色および根形状と重量のモデル化. 植物環境工学. 36(1). https://doi.org/10.2525/shita.36.12