In [1]:
require 'pycall'
# PyCall.import_module function loads a module in Python, and brings the loaded module object in Ruby
pymath = PyCall.import_module('math')
Out[1]:
In [2]:
# Accessing `sin` attribute of `math` module
pymath.sin
Out[2]:
In [3]:
# Calling function object by the syntax sugar of `.call` method call
pymath.sin.(Math::PI)
Out[3]:
Ruby 側に持ってきた Python オブジェクトは、基本的なクラスを除いてすべて PyObject クラスのインスタンスによってラップされます。
In [4]:
# pymath is a module object in Python, but it is wrapped by an instance of PyObject in Ruby
pymath.class
Out[4]:
In [5]:
# pymath.sin is a builtin-function object in Python, but it is wrapped by an instance of PyObject in Ruby
pymath.sin.class
Out[5]:
In [6]:
# The result of pymath.sin is a float object in Python, but it is automatically converted to Float object in Ruby
pymath.sin.(Math::PI).class
Out[6]:
In [7]:
# The name of a function object
pymath.sin.__name__
Out[7]:
In [8]:
# It is converted to a String object in Ruby
pymath.sin.__name__.class
Out[8]:
pycall/import
が提供する機能を利用すると、Python での import math
と同じような記法でモジュールをインポートできます。
やってみましょう
In [9]:
require 'pycall/import'
include PyCall::Import
pyimport :math
Out[9]:
In [10]:
math
Out[10]:
In [11]:
math.sin
Out[11]:
In [12]:
math.sin.(Math::PI)
Out[12]:
PyCall は PyObjectWrapper というモジュールを提供しています。このモジュールを使うと、Python のクラスに対応するラッパークラスを定義できます。ラッパークラスを定義すると、インスタンスメソッドやクラスメソッドの呼び出しを自然に記述できるようになります。
numpy を例に違いを見てみましょう。
まず、ラッパークラスを定義せずに numpy を使ってみます。
In [13]:
pyimport :numpy, as: :np
Out[13]:
In [14]:
np
Out[14]:
In [15]:
# `np.array` retrives a function object
np.array
Out[15]:
In [16]:
# Use `.call` method to call `np.array`
ary = np.array.([*1..20].map { rand })
Out[16]:
In [17]:
# This is a PyObject
ary.class
Out[17]:
In [18]:
# `ary.mean` retrieves a function object
ary.mean
Out[18]:
In [19]:
# Use `.call` method to call `ary.mean`
ary.mean.()
Out[19]:
次に、ラッパークラスを定義します。
In [20]:
module Numpy
class NDArray
include PyCall::PyObjectWrapper
wrap_class PyCall.import_module('numpy').ndarray
end
end
Out[20]:
これで Numpy::NDArray クラスが np.ndarray のラッパーになりました。
もう一度 ndarray オブジェクトを生成してみましょう。
In [21]:
ary2 = np.array.([*1..20].map { rand + 10 })
Out[21]:
In [22]:
# ary2 is a Numpy::NDArray!!
ary2.class
Out[22]:
In [23]:
# ary2.mean calls mean method!!
ary2.mean
Out[23]:
このように、PyObjectWrapper を利用して Python クラスのラッパーを Ruby 側に定義できました。 matplotlib のラッパーライブラリでは、この機能を使って Figure や Axes などのクラスのラッパーを定義しています。
残念ながら pandas の DataFrame ライブラリに対して wrap_class を適用するとエラーが出てしまう問題があります。 そのため、このチュートリアルでは pandas のラッパーを定義せずに使っていきます。
モジュールに対するラッパーを定義する機能はまだ作っていませんが、近日中に提供できる予定になっています。
In [24]:
require 'matplotlib/iruby'
Matplotlib::IRuby.activate
Out[24]:
利用するライブラリをインポートしておきましょう。
In [25]:
pyimport :pandas, as: :pd
pyimport :seaborn, as: :sns
Out[25]:
pandas のデータフレームを IRuby ノートブック上で見やすく表示するための準備をします。 これは、将来的には require 'pandas/iruby' などで自動的に実施されるようにする予定です。
In [26]:
module Pandas
class DataFrame < PyCall::PyObject
end
end
PyCall::Conversions.python_type_mapping(pd.DataFrame, Pandas::DataFrame)
dataframe_max_rows = 20
IRuby::Display::Registry.module_eval do
type { Pandas::DataFrame }
format "text/html" do |pyobj|
pyobj.to_html.(max_rows: dataframe_max_rows, show_dimensions: true, notebook: true)
end
end
In [27]:
df = sns.load_dataset.('titanic')
Out[27]:
変数 df
に代入されたオブジェクトは pandas のデータフレームです。
In [28]:
df.type
Out[28]:
データ解析の最初のステップは、データの内容を観察することから始まります。
上の表を見るとわかるように、このデータには、15個のカラムで構成されるレコードが890行あります。 これらのカラムのうち、以下のように内容が重複しているものがあります。
survived
は alive
を no
-> 0, yes
-> 1 として変換して生成したものembarked
は embark_town
の頭文字pclass
は class
を数値にしたものsex
と who
は、male
=> man
, female
=> woman
という対応関係にある内容が重複しているカラムが複数存在すると、情報量は変わらないのに処理量が増えてしまうため、これらを削除します。
In [29]:
df = df.drop.([:alive, :embark_town, :class, :who], axis: 1)
df.columns.values
Out[29]:
こうして残ったカラムは次のような意味を持っています。
カラム名 | 意味 |
---|---|
survived |
1: 生存, 0: 死亡 |
pclass |
乗客クラス (1: Upper, 2: Middle, 3: Lower) |
sex |
性別 (male : 男性, female : 女性) |
age |
年齢 (1歳未満は小数) |
sibsp |
同乗している兄弟・配偶者の人数 |
parch |
同乗している親・子供の人数 |
fare |
チケット料金 |
embarked |
乗船した都市名の頭文字 |
adult_male |
大人の男性の場合 true |
deck |
客室種別 |
alone |
一人で乗船の場合 true |
生のデータにはほぼ確実に欠損値が含まれています。このデータの場合はどうでしょうか?調べてみましょう。
データフレームの isnull
メソッドを用いると、各行各列について欠損値の場合に true
、そうで無い場合に false
を対応させた同じ形のデータフレームが作られます。そのような欠損値フラグを集めたデータフレムに対して sum
メソッドを適用することで、カラム別に欠損値の個数をカウントできます (true
を 1, false
を 0 として総和をとる)。
In [30]:
df.isnull.().sum.()
Out[30]:
これより、age
カラムには177個の欠損値、deck
カラムには688個の欠損値が存在し、その他のカラムには欠損値が無いことがわかりました。
全体で890行あるうち688個も値が欠損しているということは、deck
カラムの値は分析には使えなさそうです。
今回は deck
カラムは捨てることにします。
In [31]:
df = df.drop.(:deck, axis: 1)
nil
age
カラムの分布を見てみましょう。
In [32]:
sampled_age = df[:age].dropna.().sample.(100) # 全てのデータを使うと少し時間がかかるのでランダムサンプリングする
sns.kdeplot.(sampled_age, shade: true, cut: 0)
sns.rugplot.(sampled_age)
Out[32]:
Out[32]:
あと、平均値も見てみます。せっかくなので全カラムの要約統計量を describe
メソッドで求めましょう。
In [33]:
df.describe.()
Out[33]:
age
の平均値は 29.699118、中央値は 28 であることが分かりました。
age
の欠損値の位置を記録しておいて、ひとまず中央値を使って欠損値を埋めることにします。
In [34]:
age_isnull = df[:age].isnull.() # 欠損値の位置を記憶 (あとで使うかもしれないので)
nil
In [35]:
df[:age].fillna.(df[:age].median.(), inplace: true) # 欠損値を中央値で埋める
nil
もう一度欠損値の個数を求めてみましょう。
In [36]:
df.isnull.().sum.()
Out[36]:
残るは embarked
の2つですが、2件だけなので無視して進みます。
生存予測をするためのモデルを作るので、予測の対象となるカラムは survived
です。
まず、各カラムが survived
とどのくらい相関を持っているか見てみましょう。
そのためには、ラベルが入っている sex
と embarked
の2カラムの値を数値に変換する必要があります。
ラベル変数を数値変数へ変換したものをダミー変数と言い、pandas では get_dummies
関数を使って処理します。
In [37]:
sex_dummies = pd.get_dummies.(df[:sex])
embarked_dummies = pd.get_dummies.(df[:embarked])
df = pd.concat.(PyCall.tuple(df, sex_dummies, embarked_dummies), axis: 1)
df = df.drop.([:sex, :embarked, :S], axis: 1)
Out[37]:
sex
のダミー変数である female
と male
, および embarked
のダミー変数である C
, Q
が追加されました。
元の sex
と embarked
は削除しました。
embarked
のダミー変数にはもう一つ S
が存在していますが、C
と Q
の両方が 0 の場合、(2件ある欠損値を除いて) S
が 1 になっているはずです。ですから、S
は情報量を持たないため削除しています。
これで、全てのカラムが数値データになったので、カラム間の相関係数を corr
メソッドで求めます。
In [38]:
df.corr.()
Out[38]:
性別系のカラム (female
, male
, adult_male
) が最も相関が高いことがわかります。
In [39]:
pyfrom 'sklearn.ensemble', import: :RandomForestClassifier
pyfrom 'sklearn.linear_model', import: :LogisticRegression
pyfrom 'sklearn.svm', import: :SVC
pyfrom 'sklearn.model_selection', import: :GridSearchCV
Out[39]:
In [40]:
rfc = GridSearchCV.(
RandomForestClassifier.(n_jobs: 2),
{
n_estimators: [10, 20, 50],
max_depth: [4, 5, 6, 7],
max_features: [:auto, :log2, PyCall.None],
},
scoring: :roc_auc,
n_jobs: 4,
cv: 5
)
Out[40]:
In [41]:
x_names = [:pclass, :age, :sibsp, :parch, :fare, :adult_male, :alone, :female, :male, :C, :Q]
x = df[x_names]
y = df[:survived]
rfc.fit.(x, y)
Out[41]:
In [42]:
rfc.best_params_
Out[42]:
In [43]:
rfc.best_score_
Out[43]:
グリッドサーチおよび交差検定の結果は cv_results_
属性に入っています。この属性の値は、そのまま pandas の DataFrame に渡せます。
In [44]:
pd.DataFrame.(data: rfc.cv_results_).drop.(:params, axis: 1)
Out[44]:
もっとも成績が良かったランダムフォレストモデルにおける特徴量の重要度を見てみましょう。
もっとも成績が良いモデルは best_estimator_
で取得できます。
このモデルは RandomForestClassifier のインスタンスなので、feature_importances_
属性を持っています。
これと x_names
を seaborn の barplot を使って可視化します。
In [45]:
df_importance = pd.DataFrame.(data: {
name: x_names,
importance: rfc.best_estimator_.feature_importances_
})
sns.barplot.(x: :name, y: :importance, data: df_importance)
Out[45]:
Out[45]:
adult_male
や性別 (female
, male
) が大きく寄与していることがわかります。
逆に alone
、C
、Q
はほとんど寄与していません。
もう一度、カラム間の相関行列を見てみましょう。
In [46]:
df.corr.()
Out[46]:
adult_male
, female
, male
, はどれも0.5を超える相関係数を持っていて、かつ、特徴量としての重要度も高くなっていました。
しかし、fare
と alone
を見てみると、これらは同程度の相関係数になっていますが、特徴量としての重要度は fare
は female
と同じくらい高いのに対し、alone
はもっとも重要度が低い特徴量でした。
このように、単に相関係数を見るだけでは、特徴量が分類にどの程度重要になるかは分からないのです。
In [47]:
lrc = GridSearchCV.(
LogisticRegression.(n_jobs: 2),
{
penalty: [:l2, :l1],
C: [10.0, 1.0, 0.1, 0.01],
},
scoring: :roc_auc,
n_jobs: 4,
cv: 5
)
Out[47]:
In [48]:
lrc.fit.(x, y)
Out[48]:
In [49]:
lrc.best_params_
Out[49]:
In [50]:
pd.DataFrame.(data: lrc.cv_results_).drop.(:params, axis: 1)
Out[50]:
In [51]:
svc = GridSearchCV.(
SVC.(kernel: :rbf),
{
C: [10.0, 1.0, 0.1, 0.01],
gamma: [5, 10, 15, 20].map {|x| 1.0 / x },
},
scoring: :roc_auc,
n_jobs: 4,
cv: 5
)
Out[51]:
In [52]:
svc.fit.(x, y)
Out[52]:
In [53]:
svc.best_params_
Out[53]:
In [54]:
svc.best_score_
Out[54]:
In [55]:
pd.DataFrame.(data: svc.cv_results_).drop.(:params, axis: 1)
Out[55]:
In [56]:
result = pd.DataFrame.(data: {
model: %w[RFC LRC SVC],
score: [rfc.best_score_, lrc.best_score_, svc.best_score_]
})
sns.barplot.(x: :model, y: :score, data: result)
Out[56]:
Out[56]:
In [ ]: