Kaggle(20) - ホールドアウト法での学習・推論(LightGBM)

LightGBMというアルゴリズムを使って、タイタニックの生存予測を行います。

LightGBMは、マイクロソフトが開発したアルゴリズムで次のような特徴があります。

  • 決定木ベースの勾配ブースティングを行う
  • 精度が高い
  • 非常に高速
  • 欠損値の補完が不要
  • 特徴のスケーリング(例えば最小値0、最大値1に正規化すること)が不要

前処理

前処理(Preprocessing)では、与えられたデータセットに対してアルゴリズムを用いて予測ができる形式に変換するまでの処理を行います。

具体的には、下記の処理があげられます。

  • 欠損値の対応
  • 外れ値の検出・処理
  • ダミー変数の作成
  • 連続データの離散化
  • 特徴量選択

前処理の仕方によって、予測結果が大きく変わってきますのでとても重要であり、多くの時間を費やす作業となります。

今回は、タイタニックのデータセットに対して、不要列の削除・欠損値処理・カテゴリ変数の変換を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
import seaborn as sns
titanic = sns.load_dataset('titanic')

# 不要な列の削除
titanic.drop(['class', 'who', 'adult_male', 'deck', 'embark_town', 'alive', 'alone'], axis=1, inplace=True)

# 欠損値処理
#titanic.isnull().sum()
titanic['age'] = titanic['age'].fillna(titanic['age'].median())
titanic['embarked'] = titanic['embarked'].fillna('S')

# カテゴリ変数の変換
titanic = pd.get_dummies(titanic, columns=['sex', 'embarked'])

x_titanic = titanic.drop(['survived'], axis=1)
y_titanic = titanic['survived']

x_titanic
前処理を行ったデータセット

ホールドアウト法での学習

scikit-learnのtrain_test_split関数を用いて、データセットを67%対33%の割合でtrainセット(訓練用)とvalidセット(検証用)に分割し学習を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# x_titanicとy_titanicをtraintとvalidに分割
train_x, valid_x, train_y, valid_y = train_test_split(x_titanic, y_titanic, test_size=0.33, random_state=0)

# lgb.Datasetでtrainとvalidを作っておく
lgb_train = lgb.Dataset(train_x, train_y)
lgb_eval = lgb.Dataset(valid_x, valid_y)

# パラメータを定義
lgbm_params = {'objective': 'binary'}

# lgb.trainで学習
evals_result = {}
gbm = lgb.train(params=lgbm_params,
train_set=lgb_train,
valid_sets=[lgb_train, lgb_eval],
early_stopping_rounds=20,
evals_result=evals_result,
verbose_eval=10)

27roundでlossが最低となり、学習が終了しています。

生存予測

学習したモデルが、validセット(検証用データ)に対してどのくらい予測性能があるか確認します。

1
2
3
# valid_xについて推論
oof = (gbm.predict(valid_x) > 0.5).astype(int)
print('score', round(accuracy_score(valid_y, oof) * 100, 2))

約82.7%の正解率で予測できました。

学習の状況が、eval_resultsに格納されているので学習曲線を表示してみます。

1
2
3
4
5
import matplotlib.pyplot as plt

plt.plot(evals_result['training']['binary_logloss'], label='train_loss')
plt.plot(evals_result['valid_1']['binary_logloss'], label='valid_loss')
plt.legend()
学習曲線

train(訓練)のロスは下がり続けていますが、valid(検証)のロスは20roundあたりから下がりにくくなっています。

ロスが最も少なくなるのが27roundであり、推論はこの27roundで行われます。

predict関数のnum_iterationでroundを指定することができますが、指定しない場合はbestのroundが使われます。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

Kaggle(19) - カテゴリ変数の変換(One-Hotエンコーディング)

タイタニック・データセットの性別(sex)や乗船地(embarked)は「男・女」「C・Q・S」というカテゴリを表しており、カテゴリ間の大小関係はありません。

これらの変数のことをカテゴリ変数と呼びますが、文字のままでは扱うのが難しいので何らかの変換処理が必要となります。

データの読み込み

タイタニックのデータセットを読み込み、見やすくするために不要な列を削除しておきます。

1
2
3
4
5
6
7
import seaborn as sns
titanic = sns.load_dataset('titanic')

# 不要な列の削除
titanic.drop(['sibsp', 'parch', 'class', 'who', 'adult_male', 'deck', 'embark_town', 'alive', 'alone'], axis=1, inplace=True)

titanic

カテゴリ変数の変換(One-Hotエンコーディング)

性別(sex)項目と乗船地(embarked)項目のカテゴリ変数を変換します。

1
2
import pandas as pd
pd.get_dummies(titanic, columns=['sex', 'embarked'])

Pandasのget_dummies関数を使うと、sex項目の代わりにsex_female / sex_male項目が追加され、embarked項目の代わりにembarked_C / embarked_Q / embarked_S項目が追加されます。

該当する場合に1、非該当の場合に0が設定されます。

このような変換手法をOne-Hotエンコーディングといいます。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

Kaggle(18) - Pandas Profileによる探索的データ解析(EDA)

Pandas Profileというライブラリを使うと、数行のコードを書くだけでデータセットの概要や特徴量を決められたフォーマットで分かりやすく表示することができます。

Pandas Profileのアップデート

GoogleさんのColaboratoryでPandas Profileを実行するとバージョンの問題でエラーになるので、Pandas Profileをアップデートしておきます。

1
!pip install -U pandas_profiling

アップデート完了後に一旦ランタイムを再起動します。(Restart Runtimeボタンを押下)


Pandas Profileの実行

タイタニックのデータセットを読み込み、Pandas Profileを実行します。

なお、データ量の問題でメモリ不足のエラーが発生するので、いくつか不要そうな項目を削除しています。

1
2
3
4
5
6
7
8
9
import seaborn as sns
titanic = sns.load_dataset('titanic')

# 不要なカラムの削除
titanic.drop(['deck', 'embark_town', 'alive', 'alone'], axis=1, inplace=True)

import pandas as pd
import pandas_profiling as pdp
pdp.ProfileReport(titanic)

Pandas Profileの内容確認

Overview項目では、データセットの概要を確認できます。

  • 11列 891行のデータであること。
  • 179の欠損値(Missing)があり、全体の1.8%であること。
  • Variable Typesではカテゴリデータ(Categorical)が6項目、数値型(Numeric)が4項目、ブール型(Boolean)が1項目あること。


Variables項目では、各データ型に応じて統計量やヒストグラムが表示されます。

Toggle detailsボタンを押すと、さらに詳細なデータ(統計情報)が表示されます



Interactions項目では、変数を指定し散布図を確認することができます。



Correlations項目では、ヒートマップが表示されます。

ヒートマップは2変数間の相関係数の値を色にしたものであり、変数間の相関を一目でみることができます。



Missing Values項目では、欠損値の情報を見ることができます。

Samples項目では、先頭の10行と最後の10行が表示されます。



Duplication Rows項目では、重複行の情報を確認することができます。



Pandas Profileを使うと一気に各データの特徴を把握することができるので本当に便利ですね。

またto_file関数を使うと、ファイル出力(Html)できますのでプロファイリング結果を受け渡しする場合に有益かと思います。

1
profile.to_file('Profiling.html')

(実行環境としてGoogleさんのColaboratoryを使用ています。)

Kaggle(17) - ヒートマップ

ヒートマップは散布図と同じように相関を分析するために使用します。

メリットとしては次の通りです。

  • 相関の程度が色で表示されるため直感的に分かりやすい。
  • 多くのデータ同士の相関を一度に確認できる。

データセットのカラム数が多い場合は、まずヒートマップで分析を行い、相関がありそうなデータを絞り込んでさらに詳細に分析を行うといった手法をとることができます。

デメリットとしては、色や相関でしか表現されないのでデータの切り口を変えることで強い相関があるデータを見落とす可能性があるということです。

そのため散布図と併用して分析するのがよいと思います。

データの読み込み

まずはタイタニックのデータセットを読み込みます。

1
2
3
4
5
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
titanic = sns.load_dataset('titanic')
titanic

ヒートマップの表示

ヒートマップを表示します。

ヒートマップは数値型のデータだけを対象としています。(カテゴリデータや日付データは除外されます)

1
2
plt.figure(figsize=(9, 6))
sns.heatmap(titanic.corr(), annot=True)

生死(survived)とやや強い相関がみられるのは成人男子(adult_male)だということが見てとれます。

試しに成人女性(adult_female)と20歳未満(under_20)のカラムを追加して、ヒートマップに表示してみます。

1
2
3
4
5
6
7
8
9
10
11
12
def func(d):
#print(d.sex, d.age)
if d.sex == 'female' and d.age > 19:
return True
else:
return False

titanic['adult_female'] = titanic.apply(func, axis=1)
titanic['under_20'] = titanic.age.apply(lambda x: True if x < 20 else False)

plt.figure(figsize=(9, 6))
sns.heatmap(titanic.corr(), annot=True)

成人女性(adult_female)と生死の相関は0.41となり、やや正の相関がありそうです。

20歳未満(under_20)と生死の相関は0.096とほとんど相関関係がないことが確認できました。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

Kaggle(16) - 相関分析

2変数間のデータ分析や相関を分析するために散布図が使われます。

散布図の特徴としましては、データ1点ごとの位置や分布、相関関係を視覚的に把握できるということになります。

デメリットとしては、2変数間の分析しか行えないこと、相関の強さを視覚的にだいたい確認することしかできず、相関係数といった数値で確認できないことです。

データの読み込み

今回はチップのデータセットを読み込みます。

1
2
3
4
5
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
tips = sns.load_dataset('tips')
tips

散布図の表示

支払総額(total_bill)とチップ(tip)の散布図を表示します。

1
sns.scatterplot(data=tips, x='total_bill', y='tip')

ざっくりですが、正の相関がありそうだということが見てとれます。

相関係数を表示します。

1
tips.corr()

相関係数は0.67となり、やや相関があるようです。

(相関係数は一般的に1に近いほど正の相関が強く、-1に近いほど負の相関が強いということになります。)

さらに喫煙者と非喫煙者に絞り込んで相関を見てみます。

1
sns.scatterplot(data=tips, x='total_bill', y='tip', hue='smoker')

1つのグラフだと見づらいため、2つのグラフに分けてみます。

1
sns.relplot(data=tips, kind='scatter', x='total_bill', y='tip', col='smoker')

右図の非喫煙者の方が正の相関が強いように見えます。

非喫煙者の相関係数を見てみます。

1
tips[tips.smoker=='No'].corr()

0.82とより強い相関があることが確認できました。

さらに食事の時間帯に絞り込んで相関を確認していきます。

1
2
g = sns.FacetGrid(tips, height=5, col='time', row='smoker')
g = g.map(plt.scatter, 'total_bill', 'tip', edgecolor='w')

下側の2つの図(非喫煙者のグラフ)が上図よりも相関が強いようです。

ランチ(左)と夕食(右)を比較した場合では、どちらも同じ程度の相関があるように見えます。

そこでそれぞれの相関係数を確認します。

まず非喫煙者のランチ帯の相関係数を確認します。

1
tips[(tips.smoker=='No') & (tips.time == 'Lunch')].corr()

次に非喫煙者の夕食帯の相関係数を確認します。

1
tips[(tips.smoker=='No') & (tips.time == 'Dinner')].corr()

ランチ帯の相関係数が0.83、夕食帯の相関係数が0.81と、ややランチ帯の方が相関が強いことが分かりました。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

次回は、ヒートマップを使って大量データの相関を見ていきます。

Kaggle(15) - 時系列データの分析

時系列のデータ推移を分析するために折れ線グラフを使用します。

横軸には時間、縦軸には何らかの数値を表示し、時間の経過によって数値がどのように変化するかを確認します。

データの読み込み

今回はフライトのデータセットを読み込みます。

1
2
3
4
5
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
flights = sns.load_dataset('flights')
flights

月ごとの乗客数データであることが分かります。

折れ線グラフの表示

まずは年ごとの乗客数を折れ線グラフで表示します。

1
sns.lineplot(data=flights, x='year', y='passengers')

真ん中の折れ線が平均値で、エリア表示されているのが信頼区間(デフォルト95%)です。

(「95%信頼区間」とは、母平均が95%の確率でその範囲にあるということを表しています。)

年々乗客数が増えていることが確認できます。


次に1950年の乗客数を月ごとに表示します。

1
2
ax = sns.lineplot(data=flights[flights.year==1950], x='month', y='passengers')
ax.set_xticklabels(range(1, 13))

7、8月に乗客数が多く、11月は乗客数が少ないことが確認できます。


複数のデータを並べて表示することも可能です。

1958年以降の乗客数を月ごとに表示してみます。

1
2
ax = sns.lineplot(data=flights[flights.year>1957], x='month', y='passengers', hue='year')
ax.set_xticklabels(range(1, 13))

各月ごとにみても、年々乗客数が増えているという事が見てとれます。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

次回は、相関分析を行います。

Kaggle(14) - 箱ひげ図とバイオリン図の表示

棒グラフでは、データ間の差や数値の大小を比較しました。

箱ひげ図を使うと、最小値・最大値・中央値・第1四分位・第2四分位といった位置やどの層のボリュームが多いかといった分布を確認できます。

四分位範囲による外れ値がどのくらいあるかも視覚的に把握でき、外れ値を確認する際にも利用できます。

箱ひげ図の表示

タイタニックのデータセットを読み込みます。

1
2
3
4
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
titanic = sns.load_dataset('titanic')

箱ひげ図を表示します。

x軸に客室ランクを表示し、y軸に年齢層を表示します。

1
sns.boxplot(data=titanic, x='pclass', y='age')

次に、客室ランクを生死でスライシングしてみます。

1
sns.boxplot(data=titanic, x='pclass', y='age', hue='survived')

最後に、別グラフで男女別に表示します。

1
sns.catplot(data=titanic, kind='box', x='pclass', y='age', hue='survived', col='sex')

バイオリン図の表示

箱ひげ図ではデータがどの位置に集中しているかという分布の情報が分かりません。

バイオリン図を使うと、左右の広がりからデータの分布が把握できます。

バイオリン図を表示してみます。

1
sns.violinplot(data=titanic, x='pclass', y='age')

上記のバイオリン図から、客室ランク3の乗船客は20歳前後の乗客が多いことが分かります。


上記の図を、生死でスライシングして表示します。

1
sns.violinplot(data=titanic, x='pclass', y='age', hue='survived')

splitオプションを有効にすると、左右でそれぞれのデータ分布を確認することができます。

1
sns.violinplot(data=titanic, x='pclass', y='age', hue='survived', split=True)

さらに、別グラフで男女別に表示します。

1
sns.catplot(data=titanic, kind='violin', x='pclass', y='age', hue='survived', col='sex')

バイオリン図を使うと分布が見やすくてオススメかなと思ったのですが、上記のバイオリン図をみていてどうしてマイナスの年齢層があるのか疑問に思いました。

同じデータを表示した箱ひげ図はこんな感じです。もちろんマイナスの年齢データはありません。

少し調べてみたところバイオリン図には欠点があり「データ分布はカーネル密度推定プロットによって滑らかに描画されるため、実際にはデータが存在しない範囲にもあたかもデータが存在しているかのように見えることがある」とのことです。

今回使用したタイタニックデータだと、マイナスの年齢をここまではっきり表示させてしまうのは問題があると思うので、バイオリン図の使用は控えた方がいいとの印象を持ちました。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

次回は、時系列データの分析を行います。

Kaggle(13) - 変数間のデータ分析

棒グラフを使うと複数データ間の関係を視覚的に確認し、比較することができるようになります。

タイタニックのデータセットの場合では、客室ランクごとの年齢層がどのようになっているのかなど、客室ランク年齢の関係を分析することができます。

棒グラフの表示

タイタニックのデータセットを読み込みます。

1
2
3
4
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
titanic = sns.load_dataset('titanic')

客室ごとの年齢を棒グラフで表示します。

1
sns.barplot(data=titanic, x='pclass', y='age')

棒グラフの年齢(y軸)は平均値が表示されます。

参考までに客室ごとの平均年齢を数値で確認しておきます。

1
titanic.groupby(['pclass']).mean()

棒グラフのスライシング

次に、客室ランクを生死でスライシングしてみます。

hueオプションに生死(survived)を指定します。

1
sns.barplot(data=titanic, x='pclass', y='age', hue='survived')

いずれの客室クラスでも、生き残った人(survived=1)の数が亡くなった人(survived=0)の数よりやや少ないことが分かります。


棒グラフのスライシング(別グラフ)

スライシングの層を増やす場合に、別グラフで表示すると見やすくなります。

ここでは性別でスライシングし別グラフを表示してみます。

colオプションに性別(sex)を指定します。

1
sns.catplot(data=titanic, kind='bar', x='pclass', y='age', hue='survived', col='sex')

客室ランク2の男性の生存率がかなり低いことや、客室ランク1の女性の生存率が高いことが見てとることができます。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

次回は、箱ひげ図とバイオリン図の表示を行います。

Kaggle(12) - 単変数のデータ分析

ヒストグラムを使って単変数のデータ分析を行います。

ヒストグラムは度数分布表をグラフ化したもので、1つのデータの分布・傾向などを分析するために使用します。

ヒストグラムの表示

タイタニックのデータを読み込みます。

1
2
3
4
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
titanic = sns.load_dataset('titanic')

distplot関数を使って、タイタニック乗船客の年齢ごとの乗客数(密度:density)をヒストグラムで表示します。

密度(density)は、ヒストグラムの総面積を1としたときの割合となります。

1
sns.distplot(titanic.age)

縦棒の数はbinsオプションで指定することができます。

binsオプションを変更することによって、データ分布の見え方を変えることができます。

binsオプションに10を設定すると、大まかな分布を確認できるようになります。

1
sns.distplot(titanic.age, bins=10)

binsオプションに30を設定すると、細かな分布を確認できるようになります。

1
sns.distplot(titanic.age, bins=30)

性別でスライシングするとまた異なった特徴をみることができます。

1
2
3
4
# 性別で重ねて比較
g = sns.FacetGrid(titanic, hue='sex', height=5)
g.map(sns.distplot, 'age', kde=False)
g.add_legend()

各年齢層ごとに男性客がやや多いことが分かります。

(実行環境としてGoogleさんのColaboratoryを使用ています。)

次回は、変数間のデータ分析を行います。

Kaggle(11) - 尺度変換

データを尺度変換すると分析しやすくなくことがあります。

例えば、25歳・38歳といった年齢の数値データから20代・30代という尺度へ変換することで各年代ごとの傾向や特徴を捉えやすくなります。

他にも価格データを「高い・普通・安い」と変換したり、レビューを「高い・普通・悪い」というような尺度へ変換することもよくあります。

ヒストグラムや散布図などで可視化した場合に、見やすくなるような粒度で変換するのがおすすめです。

尺度変換

チップのデータセットを使って尺度変換を試してみます。

まずはチップのデータセットを読み込みます。

1
2
3
4
5
import seaborn as sns
from matplotlib import pyplot as pyplot
sns.set(style='darkgrid')
tips = sns.load_dataset('tips')
tips

支払い総額(total_bill)をヒストグラムで表示します。

1
sns.distplot(tips.total_bill)

支払い総額(total_bill)を4段階の尺度に分割し、箱ひげ図で表示します。

  1. low (10未満)
  2. normal (10以上25未満)
  3. high (25以上40未満)
  4. very high (40以上)
1
2
3
4
5
6
7
8
9
10
11
12
13
def convert(x):
res = 0
if x < 10:
res = 'low'
elif x < 25:
res = 'normal'
elif x < 40:
res = 'high'
else:
res = 'veryhigh'
return res
tips['total_bill_level'] = tips.total_bill.apply(convert)
sns.boxplot(data=tips.sort_values('tip'), x='total_bill_level', y='tip')

総支払額に対するチップの金額ですが、支払い金額が多いほどチップ(の中央値)が増えていることが分かります。

総支払額が少ない場合や普通程度の場合に、チップの割合が多めになってしまうこともあるようです。(外れ値が多い)

(実行環境としてGoogleさんのColaboratoryを使用ています。)