はじめに
Kaggleのチュートリアルコンペぐらいなら、Rubyでもイケるんじゃないかと思って、データ分析ライブラリのDaruと機械学習ライブラリのRumaleでTitanicコンペに挑戦してみた。
Titanic: Machine Learning from Disaster | Kaggle
DaruはPythonでいうところのPandasで、これで特徴エンジニアリングを行い、RumaleのRandom Forestで推定を行う。 DaruもRumaleもgemコマンドでインストールできる。
$ gem install daru rumale
データの読み込みと欠損値の補完
まず、KaggleのTitanicコンペからダウンロードしてきた訓練データセット(train.csv)とテストデータセット(test.csv)を読み込む。Daruでは、from_csvメソッドで、CSVファイルをDataFrameにできる。
require 'daru'
require 'rumale'
train_df = Daru::DataFrame.from_csv('train.csv', headers: false)
test_df = Daru::DataFrame.from_csv('test.csv', headers: false)
Titanicコンペでは、乗客が生存したかどうかを推定する。評価のために提出するCSVファイルは、テストデータセットの乗客IDに生存したかどうかのラベルを紐づけたものとなる。※ちなみに、コンペのもとになったタイタニック号沈没事故は痛ましい事故なので、Titanicコンペの特徴エンジニアリングを考えるときは感情移入し過ぎるとよくない。
target_vals = train_df['Survived']
train_df.delete_vectors('Survived', 'PassengerId')
test_pids = test_df['PassengerId']
test_df.delete_vectors('PassengerId')
データセットの一部には欠損値があり、これはDaruのDataFrame上ではnilとなる。replace_nils!メソッドにより任意の値で埋めることができる。
train_df['Cabin'].replace_nils!('U')
test_df['Cabin'].replace_nils!('U')
train_df['Ticket'].replace_nils!('U')
test_df['Ticket'].replace_nils!('U')
train_df['Embarked'].replace_nils!('U')
test_df['Embarked'].replace_nils!('U')
mean_age = train_df['Age'].mean
train_df['Age'].replace_nils!(mean_age)
test_df['Age'].replace_nils!(mean_age)
mean_fare = train_df['Fare'].mean
train_df['Fare'].replace_nils!(mean_fare)
test_df['Fare'].replace_nils!(mean_fare)
カテゴリデータを数値に変換する
RumaleのLabelEncoderを使用してカテゴリに数値を割り当てていく。Rumaleはデータの表現にNumo::NArrayを利用しているが、DaruはNumo::NArrayに対応していない。Rumaleとのデータの受け渡しでは、適宜to_aメソッドでArrayに変換した。
encoder = Rumale::Preprocessing::LabelEncoder.new
train_df['Embarked'] = encoder.fit_transform(train_df['Embarked'].to_a).to_a
test_df['Embarked'] = encoder.transform(test_df['Embarked'].to_a).to_a
train_df['Cabin'] = train_df['Cabin'].map { |v| v[0] }
test_df['Cabin'] = test_df['Cabin'].map { |v| v[0] }
train_df['Cabin'] = encoder.fit_transform(train_df['Cabin'].to_a).to_a
test_df['Cabin'] = encoder.transform(test_df['Cabin'].to_a).to_a
train_df['Ticket'] = train_df['Ticket'].map { |v| v.to_s[0] }
test_df['Ticket'] = test_df['Ticket'].map { |v| v.to_s[0] }
train_df['Ticket'] = encoder.fit_transform(train_df['Ticket'].to_a).to_a
test_df['Ticket'] = encoder.transform(test_df['Ticket'].to_a).to_a
バイナリ変数を追加する
Wikipediaの記事やKaggleのkernelを見ると、女性や少年は生存しているようだ。
train_df['IsFemale'] = train_df['Sex'].map { |v| v == 'female' ? 1 : 0 }
test_df['IsFemale'] = test_df['Sex'].map { |v| v == 'female' ? 1 : 0 }
train_df['IsMaster'] = train_df['Name'].map { |v| v.split(',')[1].split('.')[0].strip == 'Master' ? 1 : 0 }
test_df['IsMaster'] = test_df['Name'].map { |v| v.split(',')[1].split('.')[0].strip == 'Master' ? 1 : 0 }
数量データの一部を量子化する
特徴量としては、実数値より、ざっくりとヒストグラムで表現したほうが良い場合がある。ただ、Daruには、Pandasのqcutやcutがないようなので、なにかしらの方法で量子化する必要がある。
train_df['FamilySize'] = train_df['Parch'] + train_df['SibSp'] + 1
test_df['FamilySize'] = test_df['Parch'] + test_df['SibSp'] + 1
train_df['MeanFare'] = train_df['Fare'] / train_df['FamilySize']
test_df['MeanFare'] = test_df['Fare'] / test_df['FamilySize']
max_fare = train_df['Fare'].max.to_f
min_fare = train_df['Fare'].min.to_f
train_df['Fare'] = (((train_df['Fare'] - min_fare) / (max_fare - min_fare)) * 50.0).round
test_df['Fare'] = (((test_df['Fare'] - min_fare) / (max_fare - min_fare)) * 50.0).round
max_mean_fare = train_df['MeanFare'].max.to_f
min_mean_fare = train_df['MeanFare'].min.to_f
train_df['MeanFare'] = (((train_df['MeanFare'] - min_mean_fare) / (max_mean_fare - min_mean_fare)) * 50.0).round
test_df['MeanFare'] = (((test_df['MeanFare'] - min_mean_fare) / (max_mean_fare - min_mean_fare)) * 50.0).round
max_age = train_df['Age'].max.to_f
min_age = train_df['Age'].min.to_f
train_df['Age'] = (((train_df['Age'] - min_age) / (max_age - min_age)) * 10.0).round
test_df['Age'] = (((test_df['Age'] - min_age) / (max_age - min_age)) * 10.0).round
不要な特徴量を削除してNumo::NArray形式に変換する
Rumaleでは、データの表現にNumo::NArrayを利用している。DaruのDataFrameはto_matrixメソッドでMatrixに、Vectorはto_aメソッドでArrayに変換し、これらをNumo::NArrayにわたすことで、Numo::DFloatやNumo::Int32に変換する。
del_cols = ['Name', 'SibSp', 'Parch', 'Sex']
train_df.delete_vectors(*del_cols)
test_df.delete_vectors(*del_cols)
samples = Numo::DFloat[*train_df.to_matrix]
labels = Numo::Int32[*target_vals.to_a]
test_samples = Numo::DFloat[*test_df.to_matrix]
交差検定で確認する
Rumaleを使って交差検定を行う。特徴量の重要度も確認したいので、分類器にはRandom Forestを用いる。
imp = Numo::DFloat.zeros(n_features)
sum_accuracy = 0.0
kf = Rumale::ModelSelection::StratifiedKFold.new(n_splits: 10, shuffle: true, random_seed: 1)
kf.split(samples, labels).each do |train_ids, valid_ids|
train_samples = samples[train_ids, true]
train_labels = labels[train_ids]
valid_samples = samples[valid_ids, true]
valid_labels = labels[valid_ids]
clf = Rumale::Ensemble::RandomForestClassifier.new(n_estimators: 100, max_features: 2, random_seed: 1)
clf.fit(train_samples, train_labels)
imp += clf.feature_importances
sum_accuracy += clf.score(valid_samples, valid_labels)
end
mean_accuracy = sum_accuracy / kf.n_splits
puts
puts sprintf("Mean Accuracy: %.5f", mean_accuracy)
puts
puts "Feature importances:"
train_df.vectors.to_a.each_with_index { |col, i| puts("#{col}: #{imp[i] / kf.n_splits}") }
これを実行すると次のようになる。8割ほどの正確度が得られ、最も重要な特徴は女性であるかどうかになった。部屋や料金も重要らしい。
Mean Accuracy: 0.82270
Feature importances:
Pclass: 0.08757945953461778
Age: 0.05991353734583812
Ticket: 0.06425842380708825
Fare: 0.08939140299941103
Cabin: 0.09128477545493736
Embarked: 0.02360076253503096
IsFemale: 0.41341081597428586
IsMaster: 0.027338294741599423
FamilySize: 0.07416848338510751
MeanFare: 0.06905404422208355
提出用CSVファイルを作成する
Daruはwrite_csvメソッドで、DataFrameをCSVファイルに書き出せる。乗客IDと推定結果を結びつけて、提出用ファイルを作成する。
clf = Rumale::Ensemble::RandomForestClassifier.new(n_estimators: 100, max_features: 2, random_seed: 1)
clf.fit(samples, labels)
prediction = clf.predict(test_samples)
submission = Daru::DataFrame.new({'PassengerId': test_pids, 'Survived': prediction })
submission.write_csv('submission.csv')
これを提出すると、Public Leaderboardで0.79425だった。サンプルのgender_submission.csvがたしか0.76とかだったので、それよりは良い感じ。
おわりに
「Rumaleを作るだけじゃなく使ってみないと」と思ってTitanicコンペに挑戦してみた。特徴エンジニアリングとかは、思いつきで適当に行ったが、Titanicコンペのような小さいデータセットであれば、DaruとRumaleで十分にデータ分析などができる印象を持った。ただ、データセットが大きくなると、実行速度の面で厳しい様に思う。Rumaleは、並列に処理できる箇所があるので、Parallel gemで高速化できないか考えている。Daruは、Daru::Viewに力をいれている様子で、Daru本体のリリースが一年近くないのがちょっと気がかり 🤔
ちなみにKaggleだが、それなりのスペックのマシンを用意することをオススメする。私はEarly 2016なMacBookしかもっておらず、一時期KaggleのKernelでガンバったが、タイムアウトとかメモリ超過で強制終了になることがほとんどで、結局あきらめてしまった 😓 いつかお金と時間に余裕ができたら再挑戦したい。